diff --git a/dist/index.html b/dist/index.html
index 7346ae1..fdf30ce 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -135,6 +135,32 @@
right: -8px;
background: radial-gradient(circle at 8px 0, transparent 8px, var(--tab-active-bg) 8.5px);
}
+ /* ── Color de pestaña ────────────────────────────────────────────────────────────
+ Misma idea que los grupos de Chrome: teñir para agrupar de un vistazo («todo lo de
+ este cliente, en verde»). El color se elige con clic derecho sobre la pestaña ▸ Color,
+ y se guarda con la sesión.
+ Va en componentes RGB sueltos (`--tc: r, g, b`) para sacar de UNA sola definición
+ tanto el tono sólido como el fondo translúcido, sin repetir nueve colores por tema.
+ La pestaña ACTIVA no se tiñe entera: tiene que seguir fundida con el contenido, así
+ que lleva el color en una franja superior — igual que Chrome. */
+ .tab[data-color="gris"] { --tc: 95, 99, 104; }
+ .tab[data-color="azul"] { --tc: 26, 115, 232; }
+ .tab[data-color="rojo"] { --tc: 217, 48, 37; }
+ .tab[data-color="amarillo"] { --tc: 249, 171, 0; }
+ .tab[data-color="verde"] { --tc: 30, 142, 62; }
+ .tab[data-color="rosa"] { --tc: 208, 24, 132; }
+ .tab[data-color="morado"] { --tc: 147, 52, 230; }
+ .tab[data-color="cian"] { --tc: 0, 123, 131; }
+ .tab[data-color="naranja"] { --tc: 250, 144, 62; }
+ .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.18); }
+ .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.32); }
+ /* Con fondo propio el separador vertical sobra: ya se ve dónde acaba cada pestaña. */
+ .tab[data-color]:not(.active)::before { display: none; }
+ .tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
+ @media (prefers-color-scheme: dark) {
+ .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
+ .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
+ }
/* Reordenar arrastrando: NO se usa drag&drop de HTML5 (en el webview de la barra en
macOS los eventos no llegan), sino eventos de puntero. Se apaga el arrastre nativo de
WebKit para que no salga la imagen fantasma al mover una pestaña. */
@@ -417,6 +443,7 @@
const el = document.createElement("div");
el.className = "tab" + (t.active ? " active" : "");
el.dataset.label = t.label;
+ if (t.color) el.dataset.color = t.color; // teñido desde el menú de la pestaña
el.title = texto;
const fav = document.createElement("span");
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 17fd453..f171e1d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -66,8 +66,25 @@ struct TabRec {
title: String,
url: String,
loading: bool, // la barra pinta un giro en el icono mientras carga
+ // Color elegido a mano desde el menú de la pestaña (None = sin teñir). Aquí solo vive el
+ // NOMBRE; el tono exacto y cómo se pinta son cosa del CSS de la barra.
+ color: Option,
}
+// Paleta de colores de pestaña, la misma idea que los grupos de Chrome: sirve para agrupar de
+// un vistazo («todo lo de este cliente en verde»). El id viaja en el payload de `ls-tabs`.
+const TAB_COLORS: &[(&str, &str)] = &[
+ ("gris", "Gris"),
+ ("azul", "Azul"),
+ ("rojo", "Rojo"),
+ ("amarillo", "Amarillo"),
+ ("verde", "Verde"),
+ ("rosa", "Rosa"),
+ ("morado", "Morado"),
+ ("cian", "Cian"),
+ ("naranja", "Naranja"),
+];
+
struct Shell {
tabs: Vec,
active: usize,
@@ -107,6 +124,10 @@ struct Persisted {
win: Option,
#[serde(default)]
tabs: Vec,
+ // Color de cada pestaña, en el mismo orden que `tabs` ("" = sin teñir). Lista aparte para
+ // que un estado.json viejo (sin colores) siga cargando tal cual.
+ #[serde(default)]
+ colors: Vec,
#[serde(default)]
active: usize,
#[serde(default)]
@@ -133,6 +154,7 @@ fn save_persisted(app: &AppHandle) {
zoom_idx: Some(s.zoom_idx),
win: s.win,
tabs: s.tabs.iter().map(|t| t.url.clone()).collect(),
+ colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).collect(),
active: s.active,
autostart: s.autostart,
}
@@ -333,7 +355,7 @@ fn broadcast(app: &AppHandle) {
.tabs
.iter()
.enumerate()
- .map(|(i, t)| json!({ "label": t.label, "title": t.title, "url": t.url, "loading": t.loading, "active": i == s.active }))
+ .map(|(i, t)| json!({ "label": t.label, "title": t.title, "url": t.url, "loading": t.loading, "active": i == s.active, "color": t.color }))
.collect();
json!({
"tabs": tabs,
@@ -407,7 +429,7 @@ fn create_tab(app: &AppHandle, url: Option, activate: boo
let mut s = shell.lock().unwrap();
let label = format!("tab-{}", s.next_id);
s.next_id += 1;
- s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true });
+ s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true, color: None });
label
};
@@ -707,20 +729,35 @@ fn show_app_menu(app: &AppHandle) {
// del webview quedaría recortado). Recuerda en el estado sobre qué pestaña se abrió.
fn show_tab_menu(app: &AppHandle, label: &str) {
let Some(win) = main_window(app) else { return };
- let (idx, total) = {
+ let (idx, total, actual) = {
let shell = app.state::();
let mut s = shell.lock().unwrap();
let Some(i) = s.tabs.iter().position(|t| t.label == label) else { return };
s.menu_tab = Some(label.to_string());
- (i, s.tabs.len())
+ let actual = s.tabs[i].color.clone();
+ (i, s.tabs.len(), actual)
};
+ // Los ids se arman fuera del builder para poder pasarlos como &str (igual que el resto
+ // del menú) en vez de mezclar String y &str en el mismo constructor.
+ let ids: Vec = TAB_COLORS.iter().map(|(id, _)| format!("tab-color-{id}")).collect();
let build = || -> tauri::Result