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
f59263c parent: d4a0a1e modified
assets/style.css +4 -7 | @@ -149,10 +149,12 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | ||
| 149 | 149 | .app-header { padding: var(--space-6) var(--space-4) var(--space-3); } |
| 150 | 150 | .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; } |
| 151 | 151 | .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; | |
| 154 | 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 | 158 | /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ |
| 157 | 159 | a.team-pill { text-decoration: none; cursor: pointer; } |
| 158 | 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 | 266 | .team-choice:disabled { opacity: .55; cursor: default; } |
| 265 | 267 | .team-choice:disabled:hover { border-color: transparent; } |
| 266 | 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 | 269 | .team-choice.current { |
| 272 | 270 | background: var(--color-accent-2-200); border-color: var(--color-accent-2); |
| 273 | 271 | color: var(--color-accent-2-900); |
| 274 | 272 | } |
| 275 | -.team-choice.current .team-dot { background: var(--color-accent-2); } | |
| 276 | 273 | |
| 277 | 274 | .invite-card { |
| 278 | 275 | 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; | ||
| 7 | 7 | |
| 8 | 8 | /// Palette du design system « Organic » — couleur par ordre de création de la |
| 9 | 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 | 13 | "#d67f48", "#8fa073", "#f6a06b", "#56633f", "#a19786", "#b2622d", "#ccdbb2", |
| 14 | + "#d9b45f", "#5c7c65", "#c96f5a", "#8d7086", "#e3c088", "#6d4a33", "#7590a5", | |
| 15 | + "#c98d8d", | |
| 12 | 16 | ]; |
| 13 | 17 | |
| 14 | 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 | 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 | 33 | /// « 45 min », « 2 h », « 12 h 30 » |
| 24 | 34 | pub fn fmt_minutes(total: i64) -> String { |
| 25 | 35 | let (h, m) = (total / 60, total % 60); |
| @@ -256,4 +266,12 @@ mod tests { | ||
| 256 | 266 | assert_eq!(m[&ids[0]], PALETTE[0]); |
| 257 | 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 | } |
| @@ -7,8 +7,12 @@ use crate::db::users::UserRow; | |||
| 7 | 7 | ||
| 8 | /// Palette du design system « Organic » — couleur par ordre de création de la | 8 | /// 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 { | ||
| 23 | 23 | /// « 2 membres » |
| 24 | 24 | pub meta: String, |
| 25 | 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 | 30 | /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶). |
| @@ -45,6 +47,8 @@ pub struct Shell { | ||
| 45 | 47 | pub username: String, |
| 46 | 48 | /// Nom de l'équipe active (pill de l'en-tête). |
| 47 | 49 | pub team_name: String, |
| 50 | + /// Couleur de l'équipe active (pastille dans la pill). | |
| 51 | + pub team_color: &'static str, | |
| 48 | 52 | /// "add" | "stats" | "times" | "settings" |
| 49 | 53 | pub tab: &'static str, |
| 50 | 54 | 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}; | ||
| 14 | 14 | use axum::routing::{get, post}; |
| 15 | 15 | |
| 16 | 16 | use crate::auth::extractor::AuthUser; |
| 17 | -use crate::charts::fmt_minutes; | |
| 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}; |
| @@ -155,6 +155,7 @@ pub async fn shell( | ||
| 155 | 155 | title: title.to_string(), |
| 156 | 156 | username, |
| 157 | 157 | team_name: ctx.team_name.clone(), |
| 158 | + team_color: team_color(ctx.team_id), | |
| 158 | 159 | tab, |
| 159 | 160 | toast, |
| 160 | 161 | invite_count, |
| @@ -175,6 +176,7 @@ pub async fn shell( | ||
| 175 | 176 | .map(|t| TeamChoiceView { |
| 176 | 177 | current: t.id == ctx.team_id, |
| 177 | 178 | meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }), |
| 179 | + color: team_color(t.id), | |
| 178 | 180 | id: t.id, |
| 179 | 181 | name: t.name, |
| 180 | 182 | }) |
| @@ -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 @@ | ||
| 5 | 5 | <div class="app-topline"> |
| 6 | 6 | {% if !shell.teams.is_empty() %} |
| 7 | 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 | 9 | {{ shell.team_name }} |
| 10 | 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 | 11 | </a> |
| 12 | 12 | {% else %} |
| 13 | 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 | 15 | {{ shell.team_name }} |
| 16 | 16 | </span> |
| 17 | 17 | {% endif %} |
| @@ -74,7 +74,7 @@ | ||
| 74 | 74 | <input type="hidden" name="equipe" value="{{ t.id }}"> |
| 75 | 75 | <input type="hidden" name="retour" value="{{ shell.start_return }}"> |
| 76 | 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 | 78 | <span style="flex:1;text-align:left"> |
| 79 | 79 | <span style="display:block;font-size:16px">{{ t.name }}</span> |
| 80 | 80 | <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> |