Comparar commits

..

3 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
Munka
424f401ad6 Versión 1.6.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:58:00 -05:00
Munka
f6eea75404 La barra se pinta del color de la app: blanca en claro, negra en oscuro
Venía con el gris de Chrome (#dee1e6 / #202124) en los dos temas, y encima de una
app negra se veía como una franja de navegador pegada a la ventana en vez de parte
de ella. Ahora la franja es casi blanca en tema claro y negra en oscuro, del tono de
la app.

El truco de Chrome se conserva —la pestaña activa es una tarjeta del color del
CONTENIDO y se funde con él—, solo que en oscuro el contraste va al revés: la franja
queda MÁS negra que el contenido, así que la activa destaca por ser más clara. Quien
marca el corte de verdad es el borde inferior, que la activa tapa con su fondo.

De paso, dos ajustes que arrastraba el cambio:

- El hover de las pestañas inactivas en tema claro era blanco al 55%: sobre una
  franja casi blanca no se habría visto nada. Ahora oscurece.
- El ancho máximo de pestaña sube de 240 a 280 px. Los títulos ahora traen nombre
  real y sección («Hotjamin Orgánicos · Archivos») y a 240 se cortaban de más. Con
  muchas pestañas abiertas no cambia nada: mandan el reparto elástico y el modo
  compacto de solo icono.

Verificado: sintaxis del script con node --check y la barra montada en el navegador
con el CSS real, en los dos temas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:45:38 -05:00
Se han modificado 6 ficheros con 96 adiciones y 23 borrados

69
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
@@ -15,33 +16,60 @@
(`data-tauri-drag-region`). En Windows hay título nativo aparte y no se reserva nada. (`data-tauri-drag-region`). En Windows hay título nativo aparte y no se reserva nada.
--> -->
<style> <style>
/* Paleta estilo Chrome: la barra es una FRANJA más oscura y la pestaña activa es una /* La barra se pinta del color de la APP, no del gris de Chrome: blanca en tema claro y
tarjeta del color del contenido que se funde con él (sin línea debajo). */ negra en oscuro, para que la ventana se lea como una sola pieza y no como una franja
de navegador pegada encima.
El truco de Chrome se conserva igual —la pestaña activa es una tarjeta del color del
CONTENIDO que se funde con él—, solo que ahora el contraste va al revés en oscuro: la
franja es más negra que el contenido, así que la activa destaca por ser más clara. La
diferencia es sutil a propósito; quien la marca de verdad es el borde inferior, que la
pestaña activa tapa con su propio fondo. */
:root { :root {
--bg: #dee1e6; /* franja de pestañas (Chrome claro) */ --bg: #f4f5f7; /* franja: casi blanca, del tono de la app en claro */
--fg: #45474a; --fg: #45474a;
--muted: #5f6368; --muted: #5f6368;
--tab-active-bg: #ffffff; --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: #c9ccd1; /* línea bajo la barra (la tapa la pestaña activa) */ --border: #dcdee1; /* línea bajo la barra (la tapa la pestaña activa) */
--hover: rgba(255, 255, 255, 0.55); /* píldora de hover en pestañas inactivas */ /* Sobre una franja casi blanca, un hover blanco no se vería: aquí oscurece. */
--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: #202124; /* franja (Chrome oscuro) */ --bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
--fg: #bdc1c6; --fg: #bdc1c6;
--muted: #80868b; --muted: #80868b;
--tab-active-bg: #18181b; /* mismo fondo que la app → pestaña fundida con el contenido */ --tab-active-bg: #18181b; /* mismo fondo que la app → pestaña fundida con el contenido */
--tab-active-fg: #e8eaed; --tab-active-fg: #e8eaed;
--border: #101013; --border: #26262a;
--hover: rgba(255, 255, 255, 0.08); --hover: rgba(255, 255, 255, 0.08);
--btn-hover: rgba(255, 255, 255, 0.1); --btn-hover: rgba(255, 255, 255, 0.1);
--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;
@@ -81,10 +109,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
/* Elástico como en Chrome: se reparten el ancho y se encogen al abrir más. */ /* Elástico como en Chrome: se reparten el ancho y se encogen al abrir más. El tope
sube a 280 px porque los títulos ahora traen nombre real y sección
(«Hotjamin Orgánicos · Archivos»), y a 240 se cortaban de más. Con muchas pestañas
abiertas da igual: mandan el reparto y el modo compacto. */
flex: 1 1 220px; flex: 1 1 220px;
min-width: 42px; min-width: 42px;
max-width: 240px; max-width: 280px;
height: 30px; height: 30px;
margin-bottom: 5px; margin-bottom: 5px;
padding: 0 6px 0 10px; padding: 0 6px 0 10px;
@@ -158,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. */
@@ -498,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

@@ -1,6 +1,6 @@
{ {
"name": "labstream-desktop", "name": "labstream-desktop",
"version": "1.6.0", "version": "1.6.1",
"private": true, "private": true,
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.", "description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
"scripts": { "scripts": {

Ver fichero

@@ -2062,7 +2062,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.6.0" version = "1.6.1"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",

Ver fichero

@@ -1,6 +1,6 @@
[package] [package]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.6.0" version = "1.6.1"
description = "Cliente de escritorio de Labstream OS" description = "Cliente de escritorio de Labstream OS"
authors = ["Labstream SAS"] authors = ["Labstream SAS"]
edition = "2021" edition = "2021"

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);

Ver fichero

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Labstream OS", "productName": "Labstream OS",
"version": "1.6.0", "version": "1.6.1",
"identifier": "co.labstream.os", "identifier": "co.labstream.os",
"build": { "build": {
"frontendDist": "../dist" "frontendDist": "../dist"