Comparar commits

...

1 Commits

Autor SHA1 Mensaje Fecha
Munka
b18c155b89 La barra obedece a la app, no a Windows: tema real + acento naranja
El bug de «siempre oscura»: la web decide su modo con su propio botón
(next-themes, defaultTheme claro, sin seguir al sistema), pero la barra
elegía tema con prefers-color-scheme — el del SISTEMA. Con Windows en
oscuro y la app en claro, franja negra sobre app blanca, siempre. La
v1.6.1 pintó bien ambos temas, pero la barra seguía eligiendo el
equivocado.

El arreglo, de punta a punta:

- El script inyectado reporta en ls-title si <html> lleva la clase
  `dark`, con un MutationObserver para que la luna se refleje al
  instante. La guarda de marco y origen queda intacta (OnlyOffice).
- Rust lo guarda (global: todas las pestañas comparten modo), lo
  reenvía en ls-tabs (`dark`) y lo PERSISTE en estado.json: la app
  abre ya del color correcto, sin fogonazo.
- La barra se pone la clase `oscuro`/`claro` al recibirlo; las media
  queries quedan solo como arranque en frío (primer arranque sin
  estado.json). Los tintes de colores de pestaña cuelgan del mismo
  interruptor.
- El acento pasa de índigo #6366f1 (comentado «color de Labstream»,
  que no lo era) al naranja de marca #F47A20: aro de carga, zoom
  activo y hover de la versión.

Verificado en navegador con el CSS y el JS reales, simulando el
payload de Rust: app clara + sistema oscuro → barra clara; app
oscura → negra; sin reporte → cae al sistema. El Rust lo compila el
CI de rama (este equipo no tiene cargo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:22:46 -05:00
Se han modificado 2 ficheros con 72 adiciones y 9 borrados

39
dist/index.html vendido
Ver fichero

@@ -7,7 +7,8 @@
<!-- <!--
Barra de pestañas del shell de escritorio (webview "chrome", local). Barra de pestañas del shell de escritorio (webview "chrome", local).
Habla con Rust por eventos (__TAURI__.event): Habla con Rust por eventos (__TAURI__.event):
recibe ls-tabs {tabs:[{label,title,url,loading,active}], zoom, version, mac} recibe ls-tabs {tabs:[{label,title,url,loading,active,color}], zoom, version,
mac, win, maximized, dark}
emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle, emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle,
ls-activate-n, ls-nav, ls-menu, ls-check-update, ls-tab-menu, ls-reorder ls-activate-n, ls-nav, ls-menu, ls-check-update, ls-tab-menu, ls-reorder
En macOS la ventana lleva el título en OVERLAY: esta barra ES la fila del título, así que En macOS la ventana lleva el título en OVERLAY: esta barra ES la fila del título, así que
@@ -29,15 +30,24 @@
--muted: #5f6368; --muted: #5f6368;
--tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */ --tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */
--tab-active-fg: #202124; --tab-active-fg: #202124;
--accent: #6366f1; /* indigo-500, color de Labstream */ --accent: #F47A20; /* NARANJA Labstream (el índigo de antes no era de la marca) */
--border: #dcdee1; /* línea bajo la barra (la tapa la pestaña activa) */ --border: #dcdee1; /* línea bajo la barra (la tapa la pestaña activa) */
/* Sobre una franja casi blanca, un hover blanco no se vería: aquí oscurece. */ /* Sobre una franja casi blanca, un hover blanco no se vería: aquí oscurece. */
--hover: rgba(0, 0, 0, 0.06); --hover: rgba(0, 0, 0, 0.06);
--btn-hover: rgba(0, 0, 0, 0.08); --btn-hover: rgba(0, 0, 0, 0.08);
--sep: rgba(0, 0, 0, 0.22); /* separador fino entre pestañas inactivas */ --sep: rgba(0, 0, 0, 0.22); /* separador fino entre pestañas inactivas */
} }
/* ── Quién decide el tema ──
La APP, no Windows: la web elige su modo con su propio botón (clase `dark` en su
<html>, defaultTheme claro, sin seguir al sistema), así que obedecer aquí al
prefers-color-scheme del sistema pintaba la barra negra sobre una app clara — el
bug de siempre. El script inyectado reporta el tema real, Rust lo reenvía en
`ls-tabs` (`dark`) y este documento se pone la clase `oscuro` o `claro`.
El bloque de medios queda SOLO como arranque en frío (primer arranque, sin
estado.json y sin reporte aún): en cuanto hay clase, la clase manda.
OJO: los valores oscuros van DOS veces (media query y .oscuro) — cambiarlos a la par. */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root:not(.claro) {
--bg: #0d0d0f; /* franja: negra, más oscura que el contenido */ --bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
--fg: #bdc1c6; --fg: #bdc1c6;
--muted: #80868b; --muted: #80868b;
@@ -49,6 +59,17 @@
--sep: rgba(255, 255, 255, 0.25); --sep: rgba(255, 255, 255, 0.25);
} }
} }
:root.oscuro {
--bg: #0d0d0f;
--fg: #bdc1c6;
--muted: #80868b;
--tab-active-bg: #18181b;
--tab-active-fg: #e8eaed;
--border: #26262a;
--hover: rgba(255, 255, 255, 0.08);
--btn-hover: rgba(255, 255, 255, 0.1);
--sep: rgba(255, 255, 255, 0.25);
}
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { html, body {
margin: 0; margin: 0;
@@ -168,9 +189,11 @@
.tab[data-color]:not(.active)::before { display: none; } .tab[data-color]:not(.active)::before { display: none; }
.tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); } .tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); } :root:not(.claro) .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
.tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); } :root:not(.claro) .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
} }
:root.oscuro .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
:root.oscuro .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 /* 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 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. */ WebKit para que no salga la imagen fantasma al mover una pestaña. */
@@ -508,6 +531,12 @@
$pct.textContent = estado.zoom + "%"; $pct.textContent = estado.zoom + "%";
$pct.classList.toggle("activo", estado.zoom !== 100); $pct.classList.toggle("activo", estado.zoom !== 100);
$ver.textContent = estado.version ? "v" + estado.version : ""; $ver.textContent = estado.version ? "v" + estado.version : "";
// Tema: manda el de la APP (payload `dark`). Mientras Rust no sepa nada (null),
// no se pone clase y siguen las media queries del sistema como arranque en frío.
if (typeof estado.dark === "boolean") {
document.documentElement.classList.toggle("oscuro", estado.dark);
document.documentElement.classList.toggle("claro", !estado.dark);
}
// En macOS la barra ES la fila del título: hay que dejar libre el semáforo. // En macOS la barra ES la fila del título: hay que dejar libre el semáforo.
document.body.classList.toggle("mac", !!estado.mac); document.body.classList.toggle("mac", !!estado.mac);
// En Windows también ES la fila del título, pero ahí los botones de ventana los // En Windows también ES la fila del título, pero ahí los botones de ventana los

Ver fichero

@@ -97,11 +97,15 @@ struct Shell {
// Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA // Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA
// evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar. // evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar.
maximized: bool, maximized: bool,
// Tema REAL de la app (la clase `dark` de <html>, reportada por el script inyectado).
// Es global y no por pestaña: todas las pestañas del origen comparten el mismo modo
// (localStorage). None = ninguna pestaña ha reportado aún → la barra cae al del sistema.
dark: Option<bool>,
} }
impl Default for Shell { impl Default for Shell {
fn default() -> Self { fn default() -> Self {
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None, maximized: false } Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None, maximized: false, dark: None }
} }
} }
@@ -132,6 +136,10 @@ struct Persisted {
active: usize, active: usize,
#[serde(default)] #[serde(default)]
autostart: Option<bool>, autostart: Option<bool>,
// Último tema conocido de la app: la barra abre ya del color correcto en vez de
// arrancar con el del sistema y dar un fogonazo al cargar la primera pestaña.
#[serde(default)]
dark: Option<bool>,
} }
fn state_path<R: Runtime>(app: &AppHandle<R>) -> Option<std::path::PathBuf> { fn state_path<R: Runtime>(app: &AppHandle<R>) -> Option<std::path::PathBuf> {
@@ -157,6 +165,7 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).collect(), colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).collect(),
active: s.active, active: s.active,
autostart: s.autostart, autostart: s.autostart,
dark: s.dark,
} }
}; };
if let Some(dir) = path.parent() { if let Some(dir) = path.parent() {
@@ -212,9 +221,16 @@ const INIT_JS_TPL: &str = r#"
} }
function newTab(url) { emit('ls-new-tab', { url: url }); } function newTab(url) { emit('ls-new-tab', { url: url }); }
// Título y URL → barra de pestañas (y para restaurar la sesión al reabrir). // Título, URL y TEMA → barra de pestañas (y para restaurar la sesión al reabrir).
// El tema viaja aquí porque la web decide su modo con su propio botón (next-themes,
// clase `dark` en <html>) y NO sigue al sistema: la barra tiene que copiarle a ella.
function report() { function report() {
emit('ls-title', { label: LABEL, title: document.title || 'Labstream OS', url: location.href }); emit('ls-title', {
label: LABEL,
title: document.title || 'Labstream OS',
url: location.href,
dark: document.documentElement.classList.contains('dark')
});
} }
function watchTitle() { function watchTitle() {
var el = document.querySelector('title'); var el = document.querySelector('title');
@@ -233,6 +249,11 @@ const INIT_JS_TPL: &str = r#"
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; }; history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
}); });
addEventListener('popstate', function () { setTimeout(report, 0); }); addEventListener('popstate', function () { setTimeout(report, 0); });
// Pulsar la luna cambia la clase de <html> sin navegar: se observa para que la barra
// se entere al instante.
if (window.MutationObserver) {
new MutationObserver(report).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
// Clics en enlaces. // Clics en enlaces.
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
@@ -368,6 +389,8 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
// que dibuja ella misma los botones minimizar/maximizar/cerrar (no hay nativos). // que dibuja ella misma los botones minimizar/maximizar/cerrar (no hay nativos).
"win": cfg!(target_os = "windows"), "win": cfg!(target_os = "windows"),
"maximized": maximized, "maximized": maximized,
// Tema real de la app (null hasta el primer reporte: la barra usa el del sistema).
"dark": s.dark,
}) })
}; };
let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload); let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload);
@@ -945,6 +968,10 @@ fn field_i64(payload: &str, key: &str) -> Option<i64> {
serde_json::from_str::<serde_json::Value>(payload).ok()?.get(key)?.as_i64() serde_json::from_str::<serde_json::Value>(payload).ok()?.get(key)?.as_i64()
} }
fn field_bool(payload: &str, key: &str) -> Option<bool> {
serde_json::from_str::<serde_json::Value>(payload).ok()?.get(key)?.as_bool()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
@@ -994,6 +1021,7 @@ pub fn run() {
s.zoom_idx = saved.zoom_idx.unwrap_or(ZOOM_100).min(ZOOM_STEPS.len() - 1); s.zoom_idx = saved.zoom_idx.unwrap_or(ZOOM_100).min(ZOOM_STEPS.len() - 1);
s.win = saved.win; s.win = saved.win;
s.autostart = saved.autostart; s.autostart = saved.autostart;
s.dark = saved.dark;
} }
// Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay. // Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay.
@@ -1308,10 +1336,11 @@ pub fn run() {
}); });
let h = handle.clone(); let h = handle.clone();
handle.listen_any("ls-title", move |e| { handle.listen_any("ls-title", move |e| {
let (label, title, url) = ( let (label, title, url, dark) = (
field(e.payload(), "label"), field(e.payload(), "label"),
field(e.payload(), "title"), field(e.payload(), "title"),
field(e.payload(), "url"), field(e.payload(), "url"),
field_bool(e.payload(), "dark"),
); );
if let Some(label) = label { if let Some(label) = label {
let shell = h.state::<ShellState>(); let shell = h.state::<ShellState>();
@@ -1324,6 +1353,11 @@ pub fn run() {
t.url = url; t.url = url;
} }
} }
// El tema es global (todas las pestañas del origen comparten modo): el
// último reporte manda, venga de la pestaña que venga.
if let Some(dark) = dark {
s.dark = Some(dark);
}
drop(s); drop(s);
broadcast(&h); broadcast(&h);
save_persisted(&h); save_persisted(&h);