fix(a11y): lot P1 de l'audit WCAG — labels durée/date, contrastes AA (accent-deep pour les fonds, accent-700 pour le texte, textes atténués à 70 %), pill d'équipe teintée à 25 % (encre garantie ≥ 4.5:1 sur les 15 teintes)Unverified
Claude-Session: https://claude.ai/code/session_01FPKHKGfqvnvXKY9tNhYmkh
d60d720 parent: 1401cf3 modified
assets/style.css +38 -31 | @@ -6,6 +6,10 @@ | ||
| 6 | 6 | --color-surface: #ebddc5; |
| 7 | 7 | --color-text: #201e1d; |
| 8 | 8 | --color-accent: #c67139; |
| 9 | + /* Variante a11y : ≥ 4.5:1 avec la crème — fonds porteurs de texte crème | |
| 10 | + (boutons primaires, timer, badge). L'accent d'origine reste pour le | |
| 11 | + décoratif (focus, pastilles, sélection). */ | |
| 12 | + --color-accent-deep: #9c4f1d; | |
| 9 | 13 | --color-accent-2: #7a8a5e; |
| 10 | 14 | --color-divider: color-mix(in srgb, #201e1d 16%, transparent); |
| 11 | 15 | |
| @@ -69,7 +73,8 @@ h1, h2, h3, h4 { | ||
| 69 | 73 | line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2); |
| 70 | 74 | } |
| 71 | 75 | p { margin: 0 0 var(--space-3); } |
| 72 | -a { color: var(--color-accent); text-underline-offset: 3px; } | |
| 76 | +/* accent-700 pour le texte : l'accent d'origine plafonne à 3:1 sur crème. */ | |
| 77 | +a { color: var(--color-accent-700); text-underline-offset: 3px; } | |
| 73 | 78 | :focus { outline: none; } |
| 74 | 79 | :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } |
| 75 | 80 | ::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); } |
| @@ -78,7 +83,7 @@ input[type=number]::-webkit-outer-spin-button, | ||
| 78 | 83 | input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; } |
| 79 | 84 | input[type=number] { -moz-appearance: textfield; appearance: textfield; } |
| 80 | 85 | |
| 81 | -.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); } | |
| 86 | +.text-muted { color: color-mix(in srgb, var(--color-text) 70%, transparent); } | |
| 82 | 87 | |
| 83 | 88 | /* — boutons — */ |
| 84 | 89 | .btn { |
| @@ -92,13 +97,13 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | ||
| 92 | 97 | } |
| 93 | 98 | .btn svg { display: block; } |
| 94 | 99 | .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; } |
| 95 | -.btn-primary { background: var(--color-accent); color: var(--color-bg); } | |
| 96 | -.btn-primary:hover { background: var(--color-accent-600); } | |
| 97 | -.btn-primary:active { background: var(--color-accent-700); } | |
| 100 | +.btn-primary { background: var(--color-accent-deep); color: var(--color-bg); } | |
| 101 | +.btn-primary:hover { background: var(--color-accent-700); } | |
| 102 | +.btn-primary:active { background: var(--color-accent-900); } | |
| 98 | 103 | .btn-secondary { border-color: var(--color-divider); } |
| 99 | 104 | .btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); } |
| 100 | 105 | .btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); } |
| 101 | -.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); } | |
| 106 | +.btn-ghost { color: var(--color-accent-700); padding-inline: var(--space-1); } | |
| 102 | 107 | .btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); } |
| 103 | 108 | .btn-icon { width: 36px; height: 36px; padding: 0; } |
| 104 | 109 | .btn-icon-sm { width: 32px; height: 32px; padding: 0; } |
| @@ -124,7 +129,7 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | ||
| 124 | 129 | padding: var(--space-3); border-radius: calc(var(--radius-lg) * 1.15); |
| 125 | 130 | background: var(--color-surface); |
| 126 | 131 | } |
| 127 | -.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); } | |
| 132 | +.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-700); } | |
| 128 | 133 | .card-body { margin: 0; font-size: 13px; opacity: 0.8; } |
| 129 | 134 | |
| 130 | 135 | /* — tags — */ |
| @@ -148,19 +153,21 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | ||
| 148 | 153 | |
| 149 | 154 | .app-header { padding: var(--space-6) var(--space-4) var(--space-3); } |
| 150 | 155 | .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; } |
| 151 | -/* Fond = couleur de l'équipe (hash de l'id) et texte au meilleur contraste, | |
| 152 | - tous deux posés en style inline par le serveur. */ | |
| 156 | +/* Couleur d'équipe (hash de l'id, --team-color inline) : fond teinté à 25 % | |
| 157 | + pour garantir ≥ 4.5:1 avec l'encre quelle que soit la teinte, pastille en | |
| 158 | + couleur pleine pour l'identification. */ | |
| 153 | 159 | .team-pill { |
| 154 | 160 | display: inline-flex; align-items: center; gap: 8px; font-size: 14px; |
| 155 | - padding: 9px 15px; border-radius: 999px; | |
| 156 | - background: var(--color-accent-2-200); color: var(--color-accent-2-900); | |
| 161 | + padding: 9px 15px 9px 12px; border-radius: 999px; | |
| 162 | + background: color-mix(in srgb, var(--team-color, var(--color-accent-2)) 25%, var(--color-bg)); | |
| 163 | + color: var(--color-text); | |
| 157 | 164 | } |
| 158 | -/* Pastille couleur d'équipe (sheet « Changer d'équipe »). */ | |
| 165 | +/* Pastille couleur d'équipe (pill d'en-tête + sheet « Changer d'équipe »). */ | |
| 159 | 166 | .team-dot { width: 12px; height: 12px; border-radius: 999px; flex: none; } |
| 160 | 167 | /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ |
| 161 | 168 | a.team-pill { text-decoration: none; cursor: pointer; } |
| 162 | -a.team-pill:hover { filter: brightness(1.06); } | |
| 163 | -.app-topline .who { margin-left: auto; font-size: 12px; opacity: .6; } | |
| 169 | +a.team-pill:hover { filter: brightness(.96); } | |
| 170 | +.app-topline .who { margin-left: auto; font-size: 12px; opacity: .7; } | |
| 164 | 171 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } |
| 165 | 172 | |
| 166 | 173 | /* Dégagement bas = tab bar + bandeau timer + marge, safe area comprise. |
| @@ -187,7 +194,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 187 | 194 | font: inherit; font-size: 15px; font-weight: 700; |
| 188 | 195 | } |
| 189 | 196 | .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; } |
| 190 | -.timer-running { background: var(--color-accent); color: var(--color-bg); } | |
| 197 | +.timer-running { background: var(--color-accent-deep); color: var(--color-bg); } | |
| 191 | 198 | .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); } |
| 192 | 199 | .timer-start { |
| 193 | 200 | background: var(--color-surface); color: var(--color-accent-700); |
| @@ -210,7 +217,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 210 | 217 | font-size: 44px; line-height: 1.1; color: var(--color-accent-900); |
| 211 | 218 | } |
| 212 | 219 | .chrono-start { margin: var(--space-2) 0 0; font-size: 14px; color: var(--color-accent-900); opacity: .7; } |
| 213 | -.chrono-hint { margin: var(--space-4) 0 var(--space-6); font-size: 14px; opacity: .6; text-align: center; text-wrap: pretty; } | |
| 220 | +.chrono-hint { margin: var(--space-4) 0 var(--space-6); font-size: 14px; opacity: .7; text-align: center; text-wrap: pretty; } | |
| 214 | 221 | |
| 215 | 222 | /* Bandeau « X min mesurées » (étape 1). */ |
| 216 | 223 | .measure-card { |
| @@ -243,7 +250,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 243 | 250 | .tab { |
| 244 | 251 | flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; |
| 245 | 252 | padding: 8px 2px; border-radius: 999px; text-decoration: none; |
| 246 | - color: color-mix(in srgb, var(--color-text) 50%, transparent); | |
| 253 | + color: color-mix(in srgb, var(--color-text) 70%, transparent); | |
| 247 | 254 | font-size: 11px; |
| 248 | 255 | } |
| 249 | 256 | .tab[aria-current="page"] { color: var(--color-accent-700); font-weight: 700; } |
| @@ -251,7 +258,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 251 | 258 | .tab-badge { |
| 252 | 259 | position: absolute; top: -4px; right: -6px; |
| 253 | 260 | min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; |
| 254 | - background: var(--color-accent); color: var(--color-bg); | |
| 261 | + background: var(--color-accent-deep); color: var(--color-bg); | |
| 255 | 262 | font-size: 10px; font-weight: 700; |
| 256 | 263 | display: flex; align-items: center; justify-content: center; |
| 257 | 264 | box-shadow: 0 0 0 2px var(--color-surface); |
| @@ -304,11 +311,11 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 304 | 311 | display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-6); |
| 305 | 312 | } |
| 306 | 313 | .login h1 { font-size: 44px; margin: 0 0 var(--space-2); letter-spacing: -0.02em; } |
| 307 | -.login .tagline { font-size: 15px; opacity: .65; margin: 0 0 var(--space-8); max-width: 15em; } | |
| 314 | +.login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; } | |
| 308 | 315 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } |
| 309 | 316 | |
| 310 | 317 | /* — ajout étape 1 : liste de tâches — */ |
| 311 | -.step-hint { font-size: 13px; opacity: .6; margin: 0 0 var(--space-4); } | |
| 318 | +.step-hint { font-size: 13px; opacity: .7; margin: 0 0 var(--space-4); } | |
| 312 | 319 | .task-pick { display: grid; gap: var(--space-2); } |
| 313 | 320 | .task-pick a { |
| 314 | 321 | display: flex; align-items: center; gap: var(--space-3); |
| @@ -318,7 +325,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 318 | 325 | } |
| 319 | 326 | .task-pick a:hover { border-color: var(--color-accent); } |
| 320 | 327 | .task-pick .tname { flex: 1; } |
| 321 | -.task-pick .tweek { font-size: 12px; opacity: .5; } | |
| 328 | +.task-pick .tweek { font-size: 12px; opacity: .7; } | |
| 322 | 329 | .dot { border-radius: 999px; flex: none; display: inline-block; } |
| 323 | 330 | .dot-14 { width: 14px; height: 14px; } |
| 324 | 331 | .dot-12 { width: 12px; height: 12px; } |
| @@ -327,7 +334,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 327 | 334 | /* — ajout étape 2 — */ |
| 328 | 335 | .picked { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-6); } |
| 329 | 336 | .picked .pname { font-size: 20px; font-family: var(--font-heading); } |
| 330 | -.mini-label { font-size: 12px; opacity: .7; margin: 0 0 var(--space-2); } | |
| 337 | +.mini-label { display: block; font-size: 12px; opacity: .7; margin: 0 0 var(--space-2); } | |
| 331 | 338 | .duration-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); } |
| 332 | 339 | .stepper { |
| 333 | 340 | display: flex; align-items: center; gap: var(--space-1); |
| @@ -348,17 +355,17 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 348 | 355 | background: transparent; border-color: transparent; |
| 349 | 356 | } |
| 350 | 357 | .duration-row .unit { font-size: 15px; opacity: .7; } |
| 351 | -.duration-row .pretty { font-size: 13px; opacity: .5; } | |
| 358 | +.duration-row .pretty { font-size: 13px; opacity: .7; } | |
| 352 | 359 | .presets { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-6); } |
| 353 | 360 | .preset { |
| 354 | 361 | padding: 9px 16px; border-radius: 999px; font-size: 14px; |
| 355 | 362 | font-family: var(--font-heading); text-decoration: none; |
| 356 | 363 | border: 1px solid var(--color-divider); color: var(--color-text); |
| 357 | 364 | } |
| 358 | -.preset[aria-current="true"] { background: var(--color-accent); color: var(--color-bg); border-color: transparent; } | |
| 365 | +.preset[aria-current="true"] { background: var(--color-accent-deep); color: var(--color-bg); border-color: transparent; } | |
| 359 | 366 | .date-row { display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); } |
| 360 | 367 | .date-row .input { flex: 1; min-height: 44px; text-align: center; } |
| 361 | -.date-label { font-size: 13px; opacity: .6; margin: 0 0 var(--space-6); } | |
| 368 | +.date-label { font-size: 13px; opacity: .7; margin: 0 0 var(--space-6); } | |
| 362 | 369 | |
| 363 | 370 | /* — stats — */ |
| 364 | 371 | .legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-6); padding: 0; list-style: none; } |
| @@ -368,11 +375,11 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 368 | 375 | .chart-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-1); } |
| 369 | 376 | .chart-head h2 { margin: 0; font-size: 21px; } |
| 370 | 377 | .chart-nav { margin-left: auto; display: flex; gap: 6px; } |
| 371 | -.chart-range { font-size: 12px; opacity: .55; margin: 0 0 var(--space-4); } | |
| 372 | -.chart-empty { font-size: 14px; opacity: .55; margin: 0; padding: var(--space-6) 0; } | |
| 378 | +.chart-range { font-size: 12px; opacity: .7; margin: 0 0 var(--space-4); } | |
| 379 | +.chart-empty { font-size: 14px; opacity: .7; margin: 0; padding: var(--space-6) 0; } | |
| 373 | 380 | .cols { display: flex; align-items: flex-end; gap: var(--space-4); height: 190px; padding-top: var(--space-2); } |
| 374 | 381 | .col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; } |
| 375 | -.col .total { font-size: 12px; opacity: .6; } | |
| 382 | +.col .total { font-size: 12px; opacity: .7; } | |
| 376 | 383 | .col .user { font-size: 13px; font-weight: 600; } |
| 377 | 384 | .col-bar { |
| 378 | 385 | width: 100%; max-width: 64px; display: flex; flex-direction: column; |
| @@ -397,9 +404,9 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 397 | 404 | .sheet-head .pname { font-family: var(--font-heading); font-size: 20px; } |
| 398 | 405 | .sheet-head .close { margin-left: auto; } |
| 399 | 406 | .sheet-figures { display: flex; gap: var(--space-6); } |
| 400 | -.sheet-figures .k { font-size: 11px; opacity: .55; text-transform: uppercase; letter-spacing: .08em; } | |
| 407 | +.sheet-figures .k { font-size: 11px; opacity: .7; text-transform: uppercase; letter-spacing: .08em; } | |
| 401 | 408 | .sheet-figures .v { font-size: 30px; font-family: var(--font-heading); } |
| 402 | -.sheet-note { font-size: 13px; opacity: .6; margin: var(--space-4) 0 0; } | |
| 409 | +.sheet-note { font-size: 13px; opacity: .7; margin: var(--space-4) 0 0; } | |
| 403 | 410 | |
| 404 | 411 | /* — mes temps — */ |
| 405 | 412 | .times { list-style: none; margin: 0; padding: 0; } |
| @@ -409,7 +416,7 @@ a.team-pill:hover { filter: brightness(1.06); } | ||
| 409 | 416 | } |
| 410 | 417 | .times .tinfo { flex: 1; min-width: 0; } |
| 411 | 418 | .times .ttask { font-size: 15px; } |
| 412 | -.times .tdate { font-size: 12px; opacity: .55; } | |
| 419 | +.times .tdate { font-size: 12px; opacity: .7; } | |
| 413 | 420 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } |
| 414 | 421 | |
| 415 | 422 | /* — tâches — */ |
| @@ -6,6 +6,10 @@ | |||
| 6 | --color-surface: #ebddc5; | 6 | --color-surface: #ebddc5; |
| 7 | --color-text: #201e1d; | 7 | --color-text: #201e1d; |
| 8 | --color-accent: #c67139; | 8 | --color-accent: #c67139; |
| 9 | + /* Variante a11y : ≥ 4.5:1 avec la crème — fonds porteurs de texte crème | ||
| 10 | + (boutons primaires, timer, badge). L'accent d'origine reste pour le | ||
| 11 | + décoratif (focus, pastilles, sélection). */ | ||
| 12 | + --color-accent-deep: #9c4f1d; | ||
| 9 | --color-accent-2: #7a8a5e; | 13 | --color-accent-2: #7a8a5e; |
| 10 | --color-divider: color-mix(in srgb, #201e1d 16%, transparent); | 14 | --color-divider: color-mix(in srgb, #201e1d 16%, transparent); |
| 11 | 15 | ||
| @@ -69,7 +73,8 @@ h1, h2, h3, h4 { | |||
| 69 | line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2); | 73 | line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2); |
| 70 | } | 74 | } |
| 71 | p { margin: 0 0 var(--space-3); } | 75 | p { margin: 0 0 var(--space-3); } |
| 72 | -a { color: var(--color-accent); text-underline-offset: 3px; } | 76 | +/* accent-700 pour le texte : l'accent d'origine plafonne à 3:1 sur crème. */ |
| 77 | +a { color: var(--color-accent-700); text-underline-offset: 3px; } | ||
| 73 | :focus { outline: none; } | 78 | :focus { outline: none; } |
| 74 | :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } | 79 | :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } |
| 75 | ::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); } | 80 | ::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); } |
| @@ -78,7 +83,7 @@ input[type=number]::-webkit-outer-spin-button, | |||
| 78 | input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; } | 83 | input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; } |
| 79 | input[type=number] { -moz-appearance: textfield; appearance: textfield; } | 84 | input[type=number] { -moz-appearance: textfield; appearance: textfield; } |
| 80 | 85 | ||
| 81 | -.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); } | 86 | +.text-muted { color: color-mix(in srgb, var(--color-text) 70%, transparent); } |
| 82 | 87 | ||
| 83 | /* — boutons — */ | 88 | /* — boutons — */ |
| 84 | .btn { | 89 | .btn { |
| @@ -92,13 +97,13 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | |||
| 92 | } | 97 | } |
| 93 | .btn svg { display: block; } | 98 | .btn svg { display: block; } |
| 94 | .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; } | 99 | .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; } |
| 95 | -.btn-primary { background: var(--color-accent); color: var(--color-bg); } | 100 | +.btn-primary { background: var(--color-accent-deep); color: var(--color-bg); } |
| 96 | -.btn-primary:hover { background: var(--color-accent-600); } | 101 | +.btn-primary:hover { background: var(--color-accent-700); } |
| 97 | -.btn-primary:active { background: var(--color-accent-700); } | 102 | +.btn-primary:active { background: var(--color-accent-900); } |
| 98 | .btn-secondary { border-color: var(--color-divider); } | 103 | .btn-secondary { border-color: var(--color-divider); } |
| 99 | .btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); } | 104 | .btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); } |
| 100 | .btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); } | 105 | .btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); } |
| 101 | -.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); } | 106 | +.btn-ghost { color: var(--color-accent-700); padding-inline: var(--space-1); } |
| 102 | .btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); } | 107 | .btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); } |
| 103 | .btn-icon { width: 36px; height: 36px; padding: 0; } | 108 | .btn-icon { width: 36px; height: 36px; padding: 0; } |
| 104 | .btn-icon-sm { width: 32px; height: 32px; padding: 0; } | 109 | .btn-icon-sm { width: 32px; height: 32px; padding: 0; } |
| @@ -124,7 +129,7 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | |||
| 124 | padding: var(--space-3); border-radius: calc(var(--radius-lg) * 1.15); | 129 | padding: var(--space-3); border-radius: calc(var(--radius-lg) * 1.15); |
| 125 | background: var(--color-surface); | 130 | background: var(--color-surface); |
| 126 | } | 131 | } |
| 127 | -.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); } | 132 | +.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-700); } |
| 128 | .card-body { margin: 0; font-size: 13px; opacity: 0.8; } | 133 | .card-body { margin: 0; font-size: 13px; opacity: 0.8; } |
| 129 | 134 | ||
| 130 | /* — tags — */ | 135 | /* — tags — */ |
| @@ -148,19 +153,21 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | |||
| 148 | 153 | ||
| 149 | .app-header { padding: var(--space-6) var(--space-4) var(--space-3); } | 154 | .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; } | 155 | .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; } |
| 151 | -/* Fond = couleur de l'équipe (hash de l'id) et texte au meilleur contraste, | 156 | +/* Couleur d'équipe (hash de l'id, --team-color inline) : fond teinté à 25 % |
| 152 | - tous deux posés en style inline par le serveur. */ | 157 | + pour garantir ≥ 4.5:1 avec l'encre quelle que soit la teinte, pastille en |
| 158 | + couleur pleine pour l'identification. */ | ||
| 153 | .team-pill { | 159 | .team-pill { |
| 154 | display: inline-flex; align-items: center; gap: 8px; font-size: 14px; | 160 | display: inline-flex; align-items: center; gap: 8px; font-size: 14px; |
| 155 | - padding: 9px 15px; border-radius: 999px; | 161 | + padding: 9px 15px 9px 12px; border-radius: 999px; |
| 156 | - background: var(--color-accent-2-200); color: var(--color-accent-2-900); | 162 | + background: color-mix(in srgb, var(--team-color, var(--color-accent-2)) 25%, var(--color-bg)); |
| 163 | + color: var(--color-text); | ||
| 157 | } | 164 | } |
| 158 | -/* Pastille couleur d'équipe (sheet « Changer d'équipe »). */ | 165 | +/* Pastille couleur d'équipe (pill d'en-tête + sheet « Changer d'équipe »). */ |
| 159 | .team-dot { width: 12px; height: 12px; border-radius: 999px; flex: none; } | 166 | .team-dot { width: 12px; height: 12px; border-radius: 999px; flex: none; } |
| 160 | /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ | 167 | /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ |
| 161 | a.team-pill { text-decoration: none; cursor: pointer; } | 168 | a.team-pill { text-decoration: none; cursor: pointer; } |
| 162 | -a.team-pill:hover { filter: brightness(1.06); } | 169 | +a.team-pill:hover { filter: brightness(.96); } |
| 163 | -.app-topline .who { margin-left: auto; font-size: 12px; opacity: .6; } | 170 | +.app-topline .who { margin-left: auto; font-size: 12px; opacity: .7; } |
| 164 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } | 171 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } |
| 165 | 172 | ||
| 166 | /* Dégagement bas = tab bar + bandeau timer + marge, safe area comprise. | 173 | /* Dégagement bas = tab bar + bandeau timer + marge, safe area comprise. |
| @@ -187,7 +194,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 187 | font: inherit; font-size: 15px; font-weight: 700; | 194 | font: inherit; font-size: 15px; font-weight: 700; |
| 188 | } | 195 | } |
| 189 | .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; } | 196 | .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; } |
| 190 | -.timer-running { background: var(--color-accent); color: var(--color-bg); } | 197 | +.timer-running { background: var(--color-accent-deep); color: var(--color-bg); } |
| 191 | .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); } | 198 | .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); } |
| 192 | .timer-start { | 199 | .timer-start { |
| 193 | background: var(--color-surface); color: var(--color-accent-700); | 200 | background: var(--color-surface); color: var(--color-accent-700); |
| @@ -210,7 +217,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 210 | font-size: 44px; line-height: 1.1; color: var(--color-accent-900); | 217 | font-size: 44px; line-height: 1.1; color: var(--color-accent-900); |
| 211 | } | 218 | } |
| 212 | .chrono-start { margin: var(--space-2) 0 0; font-size: 14px; color: var(--color-accent-900); opacity: .7; } | 219 | .chrono-start { margin: var(--space-2) 0 0; font-size: 14px; color: var(--color-accent-900); opacity: .7; } |
| 213 | -.chrono-hint { margin: var(--space-4) 0 var(--space-6); font-size: 14px; opacity: .6; text-align: center; text-wrap: pretty; } | 220 | +.chrono-hint { margin: var(--space-4) 0 var(--space-6); font-size: 14px; opacity: .7; text-align: center; text-wrap: pretty; } |
| 214 | 221 | ||
| 215 | /* Bandeau « X min mesurées » (étape 1). */ | 222 | /* Bandeau « X min mesurées » (étape 1). */ |
| 216 | .measure-card { | 223 | .measure-card { |
| @@ -243,7 +250,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 243 | .tab { | 250 | .tab { |
| 244 | flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; | 251 | flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; |
| 245 | padding: 8px 2px; border-radius: 999px; text-decoration: none; | 252 | padding: 8px 2px; border-radius: 999px; text-decoration: none; |
| 246 | - color: color-mix(in srgb, var(--color-text) 50%, transparent); | 253 | + color: color-mix(in srgb, var(--color-text) 70%, transparent); |
| 247 | font-size: 11px; | 254 | font-size: 11px; |
| 248 | } | 255 | } |
| 249 | .tab[aria-current="page"] { color: var(--color-accent-700); font-weight: 700; } | 256 | .tab[aria-current="page"] { color: var(--color-accent-700); font-weight: 700; } |
| @@ -251,7 +258,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 251 | .tab-badge { | 258 | .tab-badge { |
| 252 | position: absolute; top: -4px; right: -6px; | 259 | position: absolute; top: -4px; right: -6px; |
| 253 | min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; | 260 | min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; |
| 254 | - background: var(--color-accent); color: var(--color-bg); | 261 | + background: var(--color-accent-deep); color: var(--color-bg); |
| 255 | font-size: 10px; font-weight: 700; | 262 | font-size: 10px; font-weight: 700; |
| 256 | display: flex; align-items: center; justify-content: center; | 263 | display: flex; align-items: center; justify-content: center; |
| 257 | box-shadow: 0 0 0 2px var(--color-surface); | 264 | box-shadow: 0 0 0 2px var(--color-surface); |
| @@ -304,11 +311,11 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 304 | display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-6); | 311 | display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-6); |
| 305 | } | 312 | } |
| 306 | .login h1 { font-size: 44px; margin: 0 0 var(--space-2); letter-spacing: -0.02em; } | 313 | .login h1 { font-size: 44px; margin: 0 0 var(--space-2); letter-spacing: -0.02em; } |
| 307 | -.login .tagline { font-size: 15px; opacity: .65; margin: 0 0 var(--space-8); max-width: 15em; } | 314 | +.login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; } |
| 308 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } | 315 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } |
| 309 | 316 | ||
| 310 | /* — ajout étape 1 : liste de tâches — */ | 317 | /* — ajout étape 1 : liste de tâches — */ |
| 311 | -.step-hint { font-size: 13px; opacity: .6; margin: 0 0 var(--space-4); } | 318 | +.step-hint { font-size: 13px; opacity: .7; margin: 0 0 var(--space-4); } |
| 312 | .task-pick { display: grid; gap: var(--space-2); } | 319 | .task-pick { display: grid; gap: var(--space-2); } |
| 313 | .task-pick a { | 320 | .task-pick a { |
| 314 | display: flex; align-items: center; gap: var(--space-3); | 321 | display: flex; align-items: center; gap: var(--space-3); |
| @@ -318,7 +325,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 318 | } | 325 | } |
| 319 | .task-pick a:hover { border-color: var(--color-accent); } | 326 | .task-pick a:hover { border-color: var(--color-accent); } |
| 320 | .task-pick .tname { flex: 1; } | 327 | .task-pick .tname { flex: 1; } |
| 321 | -.task-pick .tweek { font-size: 12px; opacity: .5; } | 328 | +.task-pick .tweek { font-size: 12px; opacity: .7; } |
| 322 | .dot { border-radius: 999px; flex: none; display: inline-block; } | 329 | .dot { border-radius: 999px; flex: none; display: inline-block; } |
| 323 | .dot-14 { width: 14px; height: 14px; } | 330 | .dot-14 { width: 14px; height: 14px; } |
| 324 | .dot-12 { width: 12px; height: 12px; } | 331 | .dot-12 { width: 12px; height: 12px; } |
| @@ -327,7 +334,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 327 | /* — ajout étape 2 — */ | 334 | /* — ajout étape 2 — */ |
| 328 | .picked { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-6); } | 335 | .picked { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-6); } |
| 329 | .picked .pname { font-size: 20px; font-family: var(--font-heading); } | 336 | .picked .pname { font-size: 20px; font-family: var(--font-heading); } |
| 330 | -.mini-label { font-size: 12px; opacity: .7; margin: 0 0 var(--space-2); } | 337 | +.mini-label { display: block; font-size: 12px; opacity: .7; margin: 0 0 var(--space-2); } |
| 331 | .duration-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); } | 338 | .duration-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); } |
| 332 | .stepper { | 339 | .stepper { |
| 333 | display: flex; align-items: center; gap: var(--space-1); | 340 | display: flex; align-items: center; gap: var(--space-1); |
| @@ -348,17 +355,17 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 348 | background: transparent; border-color: transparent; | 355 | background: transparent; border-color: transparent; |
| 349 | } | 356 | } |
| 350 | .duration-row .unit { font-size: 15px; opacity: .7; } | 357 | .duration-row .unit { font-size: 15px; opacity: .7; } |
| 351 | -.duration-row .pretty { font-size: 13px; opacity: .5; } | 358 | +.duration-row .pretty { font-size: 13px; opacity: .7; } |
| 352 | .presets { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-6); } | 359 | .presets { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-6); } |
| 353 | .preset { | 360 | .preset { |
| 354 | padding: 9px 16px; border-radius: 999px; font-size: 14px; | 361 | padding: 9px 16px; border-radius: 999px; font-size: 14px; |
| 355 | font-family: var(--font-heading); text-decoration: none; | 362 | font-family: var(--font-heading); text-decoration: none; |
| 356 | border: 1px solid var(--color-divider); color: var(--color-text); | 363 | border: 1px solid var(--color-divider); color: var(--color-text); |
| 357 | } | 364 | } |
| 358 | -.preset[aria-current="true"] { background: var(--color-accent); color: var(--color-bg); border-color: transparent; } | 365 | +.preset[aria-current="true"] { background: var(--color-accent-deep); color: var(--color-bg); border-color: transparent; } |
| 359 | .date-row { display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); } | 366 | .date-row { display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); } |
| 360 | .date-row .input { flex: 1; min-height: 44px; text-align: center; } | 367 | .date-row .input { flex: 1; min-height: 44px; text-align: center; } |
| 361 | -.date-label { font-size: 13px; opacity: .6; margin: 0 0 var(--space-6); } | 368 | +.date-label { font-size: 13px; opacity: .7; margin: 0 0 var(--space-6); } |
| 362 | 369 | ||
| 363 | /* — stats — */ | 370 | /* — stats — */ |
| 364 | .legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-6); padding: 0; list-style: none; } | 371 | .legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-6); padding: 0; list-style: none; } |
| @@ -368,11 +375,11 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 368 | .chart-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-1); } | 375 | .chart-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-1); } |
| 369 | .chart-head h2 { margin: 0; font-size: 21px; } | 376 | .chart-head h2 { margin: 0; font-size: 21px; } |
| 370 | .chart-nav { margin-left: auto; display: flex; gap: 6px; } | 377 | .chart-nav { margin-left: auto; display: flex; gap: 6px; } |
| 371 | -.chart-range { font-size: 12px; opacity: .55; margin: 0 0 var(--space-4); } | 378 | +.chart-range { font-size: 12px; opacity: .7; margin: 0 0 var(--space-4); } |
| 372 | -.chart-empty { font-size: 14px; opacity: .55; margin: 0; padding: var(--space-6) 0; } | 379 | +.chart-empty { font-size: 14px; opacity: .7; margin: 0; padding: var(--space-6) 0; } |
| 373 | .cols { display: flex; align-items: flex-end; gap: var(--space-4); height: 190px; padding-top: var(--space-2); } | 380 | .cols { display: flex; align-items: flex-end; gap: var(--space-4); height: 190px; padding-top: var(--space-2); } |
| 374 | .col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; } | 381 | .col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; } |
| 375 | -.col .total { font-size: 12px; opacity: .6; } | 382 | +.col .total { font-size: 12px; opacity: .7; } |
| 376 | .col .user { font-size: 13px; font-weight: 600; } | 383 | .col .user { font-size: 13px; font-weight: 600; } |
| 377 | .col-bar { | 384 | .col-bar { |
| 378 | width: 100%; max-width: 64px; display: flex; flex-direction: column; | 385 | width: 100%; max-width: 64px; display: flex; flex-direction: column; |
| @@ -397,9 +404,9 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 397 | .sheet-head .pname { font-family: var(--font-heading); font-size: 20px; } | 404 | .sheet-head .pname { font-family: var(--font-heading); font-size: 20px; } |
| 398 | .sheet-head .close { margin-left: auto; } | 405 | .sheet-head .close { margin-left: auto; } |
| 399 | .sheet-figures { display: flex; gap: var(--space-6); } | 406 | .sheet-figures { display: flex; gap: var(--space-6); } |
| 400 | -.sheet-figures .k { font-size: 11px; opacity: .55; text-transform: uppercase; letter-spacing: .08em; } | 407 | +.sheet-figures .k { font-size: 11px; opacity: .7; text-transform: uppercase; letter-spacing: .08em; } |
| 401 | .sheet-figures .v { font-size: 30px; font-family: var(--font-heading); } | 408 | .sheet-figures .v { font-size: 30px; font-family: var(--font-heading); } |
| 402 | -.sheet-note { font-size: 13px; opacity: .6; margin: var(--space-4) 0 0; } | 409 | +.sheet-note { font-size: 13px; opacity: .7; margin: var(--space-4) 0 0; } |
| 403 | 410 | ||
| 404 | /* — mes temps — */ | 411 | /* — mes temps — */ |
| 405 | .times { list-style: none; margin: 0; padding: 0; } | 412 | .times { list-style: none; margin: 0; padding: 0; } |
| @@ -409,7 +416,7 @@ a.team-pill:hover { filter: brightness(1.06); } | |||
| 409 | } | 416 | } |
| 410 | .times .tinfo { flex: 1; min-width: 0; } | 417 | .times .tinfo { flex: 1; min-width: 0; } |
| 411 | .times .ttask { font-size: 15px; } | 418 | .times .ttask { font-size: 15px; } |
| 412 | -.times .tdate { font-size: 12px; opacity: .55; } | 419 | +.times .tdate { font-size: 12px; opacity: .7; } |
| 413 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } | 420 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } |
| 414 | 421 | ||
| 415 | /* — tâches — */ | 422 | /* — tâches — */ |
modified
src/charts.rs +0 -40 | @@ -30,35 +30,6 @@ pub fn team_color(team_id: Uuid) -> &'static str { | ||
| 30 | 30 | PALETTE[(team_id.as_u128() % PALETTE.len() as u128) as usize] |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | -/// Couleur de texte au meilleur contraste WCAG sur un fond donné : l'encre ou | |
| 34 | -/// la crème du design Organic (tokens --color-text / --color-bg). | |
| 35 | -pub fn contrast_text(bg_hex: &str) -> &'static str { | |
| 36 | - const DARK: &str = "#201e1d"; | |
| 37 | - const LIGHT: &str = "#f5ead8"; | |
| 38 | - let bg = luminance(bg_hex); | |
| 39 | - if ratio(bg, luminance(DARK)) >= ratio(bg, luminance(LIGHT)) { | |
| 40 | - DARK | |
| 41 | - } else { | |
| 42 | - LIGHT | |
| 43 | - } | |
| 44 | -} | |
| 45 | - | |
| 46 | -/// Luminance relative sRGB (WCAG 2) d'une couleur « #rrggbb ». | |
| 47 | -fn luminance(hex: &str) -> f64 { | |
| 48 | - let chan = |i: usize| { | |
| 49 | - let v = u8::from_str_radix(hex.get(i..i + 2).unwrap_or("00"), 16).unwrap_or(0) as f64 | |
| 50 | - / 255.0; | |
| 51 | - if v <= 0.03928 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } | |
| 52 | - }; | |
| 53 | - 0.2126 * chan(1) + 0.7152 * chan(3) + 0.0722 * chan(5) | |
| 54 | -} | |
| 55 | - | |
| 56 | -/// Ratio de contraste WCAG entre deux luminances. | |
| 57 | -fn ratio(a: f64, b: f64) -> f64 { | |
| 58 | - let (hi, lo) = if a > b { (a, b) } else { (b, a) }; | |
| 59 | - (hi + 0.05) / (lo + 0.05) | |
| 60 | -} | |
| 61 | - | |
| 62 | 33 | /// « 45 min », « 2 h », « 12 h 30 » |
| 63 | 34 | pub fn fmt_minutes(total: i64) -> String { |
| 64 | 35 | let (h, m) = (total / 60, total % 60); |
| @@ -303,15 +274,4 @@ mod tests { | ||
| 303 | 274 | assert!(PALETTE.contains(&team_color(id))); |
| 304 | 275 | assert_eq!(team_color(Uuid::from_u128(8)), PALETTE[8]); |
| 305 | 276 | } |
| 306 | - | |
| 307 | - #[test] | |
| 308 | - fn contrast_text_picks_readable_ink() { | |
| 309 | - assert_eq!(contrast_text("#56633f"), "#f5ead8"); // olive sombre → crème | |
| 310 | - assert_eq!(contrast_text("#6d4a33"), "#f5ead8"); // cacao → crème | |
| 311 | - assert_eq!(contrast_text("#ccdbb2"), "#201e1d"); // vert pâle → encre | |
| 312 | - assert_eq!(contrast_text("#e3c088"), "#201e1d"); // sable → encre | |
| 313 | - for bg in PALETTE { | |
| 314 | - assert!(["#201e1d", "#f5ead8"].contains(&contrast_text(bg))); | |
| 315 | - } | |
| 316 | - } | |
| 317 | 277 | } |
| @@ -30,35 +30,6 @@ pub fn team_color(team_id: Uuid) -> &'static str { | |||
| 30 | PALETTE[(team_id.as_u128() % PALETTE.len() as u128) as usize] | 30 | PALETTE[(team_id.as_u128() % PALETTE.len() as u128) as usize] |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | -/// Couleur de texte au meilleur contraste WCAG sur un fond donné : l'encre ou | ||
| 34 | -/// la crème du design Organic (tokens --color-text / --color-bg). | ||
| 35 | -pub fn contrast_text(bg_hex: &str) -> &'static str { | ||
| 36 | - const DARK: &str = "#201e1d"; | ||
| 37 | - const LIGHT: &str = "#f5ead8"; | ||
| 38 | - let bg = luminance(bg_hex); | ||
| 39 | - if ratio(bg, luminance(DARK)) >= ratio(bg, luminance(LIGHT)) { | ||
| 40 | - DARK | ||
| 41 | - } else { | ||
| 42 | - LIGHT | ||
| 43 | - } | ||
| 44 | -} | ||
| 45 | - | ||
| 46 | -/// Luminance relative sRGB (WCAG 2) d'une couleur « #rrggbb ». | ||
| 47 | -fn luminance(hex: &str) -> f64 { | ||
| 48 | - let chan = |i: usize| { | ||
| 49 | - let v = u8::from_str_radix(hex.get(i..i + 2).unwrap_or("00"), 16).unwrap_or(0) as f64 | ||
| 50 | - / 255.0; | ||
| 51 | - if v <= 0.03928 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } | ||
| 52 | - }; | ||
| 53 | - 0.2126 * chan(1) + 0.7152 * chan(3) + 0.0722 * chan(5) | ||
| 54 | -} | ||
| 55 | - | ||
| 56 | -/// Ratio de contraste WCAG entre deux luminances. | ||
| 57 | -fn ratio(a: f64, b: f64) -> f64 { | ||
| 58 | - let (hi, lo) = if a > b { (a, b) } else { (b, a) }; | ||
| 59 | - (hi + 0.05) / (lo + 0.05) | ||
| 60 | -} | ||
| 61 | - | ||
| 62 | /// « 45 min », « 2 h », « 12 h 30 » | 33 | /// « 45 min », « 2 h », « 12 h 30 » |
| 63 | pub fn fmt_minutes(total: i64) -> String { | 34 | pub fn fmt_minutes(total: i64) -> String { |
| 64 | let (h, m) = (total / 60, total % 60); | 35 | let (h, m) = (total / 60, total % 60); |
| @@ -303,15 +274,4 @@ mod tests { | |||
| 303 | assert!(PALETTE.contains(&team_color(id))); | 274 | assert!(PALETTE.contains(&team_color(id))); |
| 304 | assert_eq!(team_color(Uuid::from_u128(8)), PALETTE[8]); | 275 | assert_eq!(team_color(Uuid::from_u128(8)), PALETTE[8]); |
| 305 | } | 276 | } |
| 306 | - | ||
| 307 | - #[test] | ||
| 308 | - fn contrast_text_picks_readable_ink() { | ||
| 309 | - assert_eq!(contrast_text("#56633f"), "#f5ead8"); // olive sombre → crème | ||
| 310 | - assert_eq!(contrast_text("#6d4a33"), "#f5ead8"); // cacao → crème | ||
| 311 | - assert_eq!(contrast_text("#ccdbb2"), "#201e1d"); // vert pâle → encre | ||
| 312 | - assert_eq!(contrast_text("#e3c088"), "#201e1d"); // sable → encre | ||
| 313 | - for bg in PALETTE { | ||
| 314 | - assert!(["#201e1d", "#f5ead8"].contains(&contrast_text(bg))); | ||
| 315 | - } | ||
| 316 | - } | ||
| 317 | } | 277 | } |
modified
src/render.rs +1 -3 | @@ -47,10 +47,8 @@ pub struct Shell { | ||
| 47 | 47 | pub username: String, |
| 48 | 48 | /// Nom de l'équipe active (pill de l'en-tête). |
| 49 | 49 | pub team_name: String, |
| 50 | - /// Couleur de l'équipe active (fond de la pill d'en-tête). | |
| 50 | + /// Couleur de l'équipe active (teinte de la pill d'en-tête). | |
| 51 | 51 | pub team_color: &'static str, |
| 52 | - /// Encre ou crème selon le meilleur contraste sur team_color. | |
| 53 | - pub team_text: &'static str, | |
| 54 | 52 | /// "add" | "stats" | "times" | "settings" |
| 55 | 53 | pub tab: &'static str, |
| 56 | 54 | pub toast: Option<String>, |
| @@ -47,10 +47,8 @@ pub struct Shell { | |||
| 47 | pub username: String, | 47 | pub username: String, |
| 48 | /// Nom de l'équipe active (pill de l'en-tête). | 48 | /// Nom de l'équipe active (pill de l'en-tête). |
| 49 | pub team_name: String, | 49 | pub team_name: String, |
| 50 | - /// Couleur de l'équipe active (fond de la pill d'en-tête). | 50 | + /// Couleur de l'équipe active (teinte de la pill d'en-tête). |
| 51 | pub team_color: &'static str, | 51 | pub team_color: &'static str, |
| 52 | - /// Encre ou crème selon le meilleur contraste sur team_color. | ||
| 53 | - pub team_text: &'static str, | ||
| 54 | /// "add" | "stats" | "times" | "settings" | 52 | /// "add" | "stats" | "times" | "settings" |
| 55 | pub tab: &'static str, | 53 | pub tab: &'static str, |
| 56 | pub toast: Option<String>, | 54 | pub toast: Option<String>, |
modified
src/routes/mod.rs +1 -2 | @@ -14,7 +14,7 @@ use axum::response::{Redirect, Response}; | ||
| 14 | 14 | use axum::routing::{get, post}; |
| 15 | 15 | |
| 16 | 16 | use crate::auth::extractor::AuthUser; |
| 17 | -use crate::charts::{contrast_text, fmt_minutes, team_color}; | |
| 17 | +use crate::charts::{fmt_minutes, team_color}; | |
| 18 | 18 | use crate::db::{invitations, teams as teams_db, timers, users}; |
| 19 | 19 | use crate::error::AppError; |
| 20 | 20 | use crate::render::{Shell, TeamChoiceView, TimerView}; |
| @@ -156,7 +156,6 @@ pub async fn shell( | ||
| 156 | 156 | username, |
| 157 | 157 | team_name: ctx.team_name.clone(), |
| 158 | 158 | team_color: team_color(ctx.team_id), |
| 159 | - team_text: contrast_text(team_color(ctx.team_id)), | |
| 160 | 159 | tab, |
| 161 | 160 | toast, |
| 162 | 161 | invite_count, |
| @@ -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::{contrast_text, fmt_minutes, team_color}; | 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}; |
| @@ -156,7 +156,6 @@ pub async fn shell( | |||
| 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 | team_color: team_color(ctx.team_id), |
| 159 | - team_text: contrast_text(team_color(ctx.team_id)), | ||
| 160 | tab, | 159 | tab, |
| 161 | toast, | 160 | toast, |
| 162 | invite_count, | 161 | invite_count, |
modified
templates/add_step2.html +3 -3 | @@ -10,7 +10,7 @@ | ||
| 10 | 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 11 | 11 | <input type="hidden" name="tache" value="{{ tache }}"> |
| 12 | 12 | |
| 13 | - <p class="mini-label">Durée</p> | |
| 13 | + <label class="mini-label" for="minutes">Durée</label> | |
| 14 | 14 | <div class="duration-row"> |
| 15 | 15 | <div class="stepper"> |
| 16 | 16 | <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="Cinq minutes de moins"> |
| @@ -75,12 +75,12 @@ | ||
| 75 | 75 | {% endfor %} |
| 76 | 76 | </div> |
| 77 | 77 | |
| 78 | - <p class="mini-label">Date</p> | |
| 78 | + <label class="mini-label" for="date">Date</label> | |
| 79 | 79 | <div class="date-row"> |
| 80 | 80 | <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="Jour précédent"> |
| 81 | 81 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 82 | 82 | </a> |
| 83 | - <input class="input" name="date" type="date" value="{{ date }}" required> | |
| 83 | + <input class="input" id="date" name="date" type="date" value="{{ date }}" required> | |
| 84 | 84 | {% if let Some(next) = day_next %} |
| 85 | 85 | <a class="btn btn-secondary btn-icon" href="{{ next }}" aria-label="Jour suivant"> |
| 86 | 86 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
| @@ -10,7 +10,7 @@ | |||
| 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 11 | <input type="hidden" name="tache" value="{{ tache }}"> | 11 | <input type="hidden" name="tache" value="{{ tache }}"> |
| 12 | 12 | ||
| 13 | - <p class="mini-label">Durée</p> | 13 | + <label class="mini-label" for="minutes">Durée</label> |
| 14 | <div class="duration-row"> | 14 | <div class="duration-row"> |
| 15 | <div class="stepper"> | 15 | <div class="stepper"> |
| 16 | <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="Cinq minutes de moins"> | 16 | <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="Cinq minutes de moins"> |
| @@ -75,12 +75,12 @@ | |||
| 75 | {% endfor %} | 75 | {% endfor %} |
| 76 | </div> | 76 | </div> |
| 77 | 77 | ||
| 78 | - <p class="mini-label">Date</p> | 78 | + <label class="mini-label" for="date">Date</label> |
| 79 | <div class="date-row"> | 79 | <div class="date-row"> |
| 80 | <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="Jour précédent"> | 80 | <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="Jour précédent"> |
| 81 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> | 81 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 82 | </a> | 82 | </a> |
| 83 | - <input class="input" name="date" type="date" value="{{ date }}" required> | 83 | + <input class="input" id="date" name="date" type="date" value="{{ date }}" required> |
| 84 | {% if let Some(next) = day_next %} | 84 | {% if let Some(next) = day_next %} |
| 85 | <a class="btn btn-secondary btn-icon" href="{{ next }}" aria-label="Jour suivant"> | 85 | <a class="btn btn-secondary btn-icon" href="{{ next }}" aria-label="Jour suivant"> |
| 86 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> | 86 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
modified
templates/app.html +5 -3 | @@ -4,12 +4,14 @@ | ||
| 4 | 4 | <header class="app-header"> |
| 5 | 5 | <div class="app-topline"> |
| 6 | 6 | {% if !shell.teams.is_empty() %} |
| 7 | - <a class="team-pill" href="#equipes" aria-label="Changer d'équipe" style="background:{{ shell.team_color }};color:{{ shell.team_text }}"> | |
| 7 | + <a class="team-pill" href="#equipes" aria-label="Changer d'équipe" style="--team-color:{{ shell.team_color }}"> | |
| 8 | + <span class="team-dot" style="background:{{ shell.team_color }}"></span> | |
| 8 | 9 | {{ shell.team_name }} |
| 9 | 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 | 11 | </a> |
| 11 | 12 | {% else %} |
| 12 | - <span class="team-pill" style="background:{{ shell.team_color }};color:{{ shell.team_text }}"> | |
| 13 | + <span class="team-pill" style="--team-color:{{ shell.team_color }}"> | |
| 14 | + <span class="team-dot" style="background:{{ shell.team_color }}"></span> | |
| 13 | 15 | {{ shell.team_name }} |
| 14 | 16 | </span> |
| 15 | 17 | {% endif %} |
| @@ -75,7 +77,7 @@ | ||
| 75 | 77 | <span class="team-dot" style="background:{{ t.color }}"></span> |
| 76 | 78 | <span style="flex:1;text-align:left"> |
| 77 | 79 | <span style="display:block;font-size:16px">{{ t.name }}</span> |
| 78 | - <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | |
| 80 | + <span style="display:block;font-size:12px;opacity:.7">{{ t.meta }}</span> | |
| 79 | 81 | </span> |
| 80 | 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 %} |
| 81 | 83 | </button> |
| @@ -4,12 +4,14 @@ | |||
| 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() %} | 6 | {% if !shell.teams.is_empty() %} |
| 7 | - <a class="team-pill" href="#equipes" aria-label="Changer d'équipe" style="background:{{ shell.team_color }};color:{{ shell.team_text }}"> | 7 | + <a class="team-pill" href="#equipes" aria-label="Changer d'équipe" style="--team-color:{{ shell.team_color }}"> |
| 8 | + <span class="team-dot" style="background:{{ shell.team_color }}"></span> | ||
| 8 | {{ shell.team_name }} | 9 | {{ shell.team_name }} |
| 9 | <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> |
| 10 | </a> | 11 | </a> |
| 11 | {% else %} | 12 | {% else %} |
| 12 | - <span class="team-pill" style="background:{{ shell.team_color }};color:{{ shell.team_text }}"> | 13 | + <span class="team-pill" style="--team-color:{{ shell.team_color }}"> |
| 14 | + <span class="team-dot" style="background:{{ shell.team_color }}"></span> | ||
| 13 | {{ shell.team_name }} | 15 | {{ shell.team_name }} |
| 14 | </span> | 16 | </span> |
| 15 | {% endif %} | 17 | {% endif %} |
| @@ -75,7 +77,7 @@ | |||
| 75 | <span class="team-dot" style="background:{{ t.color }}"></span> | 77 | <span class="team-dot" style="background:{{ t.color }}"></span> |
| 76 | <span style="flex:1;text-align:left"> | 78 | <span style="flex:1;text-align:left"> |
| 77 | <span style="display:block;font-size:16px">{{ t.name }}</span> | 79 | <span style="display:block;font-size:16px">{{ t.name }}</span> |
| 78 | - <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | 80 | + <span style="display:block;font-size:12px;opacity:.7">{{ t.meta }}</span> |
| 79 | </span> | 81 | </span> |
| 80 | {% 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 %} | 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 %} |
| 81 | </button> | 83 | </button> |
modified
templates/settings.html +5 -5 | @@ -37,7 +37,7 @@ | ||
| 37 | 37 | </div> |
| 38 | 38 | </div> |
| 39 | 39 | </form> |
| 40 | -<p style="font-size:12px;opacity:.55;margin:0 0 var(--space-6)">Le nouveau nom est visible par tous les membres.</p> | |
| 40 | +<p style="font-size:12px;opacity:.7;margin:0 0 var(--space-6)">Le nouveau nom est visible par tous les membres.</p> | |
| 41 | 41 | |
| 42 | 42 | <h4 style="margin:0 0 var(--space-2)">Membres de {{ shell.team_name }}</h4> |
| 43 | 43 | <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-4)"> |
| @@ -55,7 +55,7 @@ | ||
| 55 | 55 | </div> |
| 56 | 56 | </form> |
| 57 | 57 | {% if let Some(e) = invite_error %}<p class="form-error" style="margin:0 0 var(--space-2)">{{ e }}</p>{% endif %} |
| 58 | -<p style="font-size:12px;opacity:.55;margin:0">L'utilisateur doit déjà avoir un compte. Il reçoit une invitation à accepter de son côté.</p> | |
| 58 | +<p style="font-size:12px;opacity:.7;margin:0">L'utilisateur doit déjà avoir un compte. Il reçoit une invitation à accepter de son côté.</p> | |
| 59 | 59 | |
| 60 | 60 | {% if !sent.is_empty() %} |
| 61 | 61 | <h4 style="margin:var(--space-6) 0 var(--space-2)">Invitations en attente</h4> |
| @@ -63,7 +63,7 @@ | ||
| 63 | 63 | <div class="sent-row"> |
| 64 | 64 | <div style="flex:1;min-width:0"> |
| 65 | 65 | <div style="font-size:15px">{{ si.name }}</div> |
| 66 | - <div style="font-size:12px;opacity:.55">{{ si.meta }}</div> | |
| 66 | + <div style="font-size:12px;opacity:.7">{{ si.meta }}</div> | |
| 67 | 67 | </div> |
| 68 | 68 | <form method="post" action="{{ si.cancel_action }}" style="margin:0"> |
| 69 | 69 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| @@ -81,7 +81,7 @@ | ||
| 81 | 81 | <button type="submit" class="btn btn-primary" style="min-height:44px">Créer</button> |
| 82 | 82 | </div> |
| 83 | 83 | </form> |
| 84 | -<p style="font-size:12px;opacity:.55;margin:var(--space-2) 0 0">Tu bascules dessus aussitôt — invite ensuite qui tu veux.</p> | |
| 84 | +<p style="font-size:12px;opacity:.7;margin:var(--space-2) 0 0">Tu bascules dessus aussitôt — invite ensuite qui tu veux.</p> | |
| 85 | 85 | |
| 86 | 86 | {% if can_leave %} |
| 87 | 87 | <form method="post" action="/equipes/quitter"> |
| @@ -89,7 +89,7 @@ | ||
| 89 | 89 | <button type="submit" class="btn btn-block" |
| 90 | 90 | style="min-height:46px;margin-top:var(--space-6);color:var(--color-accent-700);border:1px solid var(--color-accent-300)">Quitter « {{ shell.team_name }} »</button> |
| 91 | 91 | </form> |
| 92 | -<p style="font-size:11px;opacity:.45;margin-top:var(--space-2);text-align:center">Tes temps déjà saisis restent dans les stats de l'équipe.</p> | |
| 92 | +<p style="font-size:11px;opacity:.7;margin-top:var(--space-2);text-align:center">Tes temps déjà saisis restent dans les stats de l'équipe.</p> | |
| 93 | 93 | {% endif %} |
| 94 | 94 | |
| 95 | 95 | <h4 style="margin:var(--space-8) 0 var(--space-3)">Session</h4> |
| @@ -37,7 +37,7 @@ | |||
| 37 | </div> | 37 | </div> |
| 38 | </div> | 38 | </div> |
| 39 | </form> | 39 | </form> |
| 40 | -<p style="font-size:12px;opacity:.55;margin:0 0 var(--space-6)">Le nouveau nom est visible par tous les membres.</p> | 40 | +<p style="font-size:12px;opacity:.7;margin:0 0 var(--space-6)">Le nouveau nom est visible par tous les membres.</p> |
| 41 | 41 | ||
| 42 | <h4 style="margin:0 0 var(--space-2)">Membres de {{ shell.team_name }}</h4> | 42 | <h4 style="margin:0 0 var(--space-2)">Membres de {{ shell.team_name }}</h4> |
| 43 | <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-4)"> | 43 | <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-4)"> |
| @@ -55,7 +55,7 @@ | |||
| 55 | </div> | 55 | </div> |
| 56 | </form> | 56 | </form> |
| 57 | {% if let Some(e) = invite_error %}<p class="form-error" style="margin:0 0 var(--space-2)">{{ e }}</p>{% endif %} | 57 | {% if let Some(e) = invite_error %}<p class="form-error" style="margin:0 0 var(--space-2)">{{ e }}</p>{% endif %} |
| 58 | -<p style="font-size:12px;opacity:.55;margin:0">L'utilisateur doit déjà avoir un compte. Il reçoit une invitation à accepter de son côté.</p> | 58 | +<p style="font-size:12px;opacity:.7;margin:0">L'utilisateur doit déjà avoir un compte. Il reçoit une invitation à accepter de son côté.</p> |
| 59 | 59 | ||
| 60 | {% if !sent.is_empty() %} | 60 | {% if !sent.is_empty() %} |
| 61 | <h4 style="margin:var(--space-6) 0 var(--space-2)">Invitations en attente</h4> | 61 | <h4 style="margin:var(--space-6) 0 var(--space-2)">Invitations en attente</h4> |
| @@ -63,7 +63,7 @@ | |||
| 63 | <div class="sent-row"> | 63 | <div class="sent-row"> |
| 64 | <div style="flex:1;min-width:0"> | 64 | <div style="flex:1;min-width:0"> |
| 65 | <div style="font-size:15px">{{ si.name }}</div> | 65 | <div style="font-size:15px">{{ si.name }}</div> |
| 66 | - <div style="font-size:12px;opacity:.55">{{ si.meta }}</div> | 66 | + <div style="font-size:12px;opacity:.7">{{ si.meta }}</div> |
| 67 | </div> | 67 | </div> |
| 68 | <form method="post" action="{{ si.cancel_action }}" style="margin:0"> | 68 | <form method="post" action="{{ si.cancel_action }}" style="margin:0"> |
| 69 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 69 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| @@ -81,7 +81,7 @@ | |||
| 81 | <button type="submit" class="btn btn-primary" style="min-height:44px">Créer</button> | 81 | <button type="submit" class="btn btn-primary" style="min-height:44px">Créer</button> |
| 82 | </div> | 82 | </div> |
| 83 | </form> | 83 | </form> |
| 84 | -<p style="font-size:12px;opacity:.55;margin:var(--space-2) 0 0">Tu bascules dessus aussitôt — invite ensuite qui tu veux.</p> | 84 | +<p style="font-size:12px;opacity:.7;margin:var(--space-2) 0 0">Tu bascules dessus aussitôt — invite ensuite qui tu veux.</p> |
| 85 | 85 | ||
| 86 | {% if can_leave %} | 86 | {% if can_leave %} |
| 87 | <form method="post" action="/equipes/quitter"> | 87 | <form method="post" action="/equipes/quitter"> |
| @@ -89,7 +89,7 @@ | |||
| 89 | <button type="submit" class="btn btn-block" | 89 | <button type="submit" class="btn btn-block" |
| 90 | style="min-height:46px;margin-top:var(--space-6);color:var(--color-accent-700);border:1px solid var(--color-accent-300)">Quitter « {{ shell.team_name }} »</button> | 90 | style="min-height:46px;margin-top:var(--space-6);color:var(--color-accent-700);border:1px solid var(--color-accent-300)">Quitter « {{ shell.team_name }} »</button> |
| 91 | </form> | 91 | </form> |
| 92 | -<p style="font-size:11px;opacity:.45;margin-top:var(--space-2);text-align:center">Tes temps déjà saisis restent dans les stats de l'équipe.</p> | 92 | +<p style="font-size:11px;opacity:.7;margin-top:var(--space-2);text-align:center">Tes temps déjà saisis restent dans les stats de l'équipe.</p> |
| 93 | {% endif %} | 93 | {% endif %} |
| 94 | 94 | ||
| 95 | <h4 style="margin:var(--space-8) 0 var(--space-3)">Session</h4> | 95 | <h4 style="margin:var(--space-8) 0 var(--space-3)">Session</h4> |