Files
labstream-desktop/dist/index.html
Jonathan David Florez Salomón 73486aa3fe La esquina de la pestaña activa deja de romperse, y las que esperan se hunden
DOS COSAS EN LA BARRA DE PESTAÑAS.

1) LA ESQUINA. La activa llevaba dos pseudo-elementos de 8px a los lados con un
   degradado radial: la curva cóncava de Chrome. El truco funciona en Chrome
   porque allí NO hay línea bajo la barra; aquí sí la hay, y esas dos piezas
   blancas la partían en dos justo a los costados de la pestaña — la fluctuación
   que se veía en la esquina. Se retiran: la activa ya se funde con el contenido
   por su propio fondo, que tapa la línea exactamente por su ancho, y sin las
   piezas no asoma nada.

2) LA PROFUNDIDAD. La inactiva no era nada: texto suelto sobre la franja, al
   mismo plano que la activa, separado por un filete de 1px. Ahora se HUNDE —velo
   muy tenue y sombra interior— y al pasar el ratón el gesto se invierte: la
   sombra sale hacia fuera y el fondo tira hacia el blanco de la activa, así que
   la pestaña sube hacia ti. Es sombra y no relleno sólido a propósito: un bloque
   gris pesa tanto como la activa y acaba compitiendo con ella.

   La activa gana su propia sombra, HACIA ARRIBA: abajo está fundida con el
   contenido y una sombra ahí dibujaría una línea donde no debe haberla.

   Con fondo propio el separador vertical sobra y se retira. `--sep` se queda:
   lo sigue usando el aro del favicon mientras carga.

Sin cambio de versión: queda listo para la próxima que se saque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:20:30 -05:00

639 líneas
30 KiB
HTML
Original Blame Histórico

Este archivo contiene caracteres Unicode ambiguos

Este archivo contiene caracteres Unicode que pueden confundirse con otros caracteres. Si crees que esto es intencional, puedes ignorar esta advertencia. Usa el botón de Escape para revelarlos.

<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Labstream OS</title>
<!--
Barra de pestañas del shell de escritorio (webview "chrome", local).
Habla con Rust por eventos (__TAURI__.event):
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,
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
deja libre el hueco del semáforo (clase `mac`) y el espacio vacío arrastra la ventana
(`data-tauri-drag-region`). En Windows hay título nativo aparte y no se reserva nada.
-->
<style>
/* La barra se pinta del color de la APP, no del gris de Chrome: blanca en tema claro y
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 {
--bg: #f4f5f7; /* franja: casi blanca, del tono de la app en claro */
--fg: #45474a;
--muted: #5f6368;
--tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */
--tab-active-fg: #202124;
--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) */
/* 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);
--sep: rgba(0, 0, 0, 0.22); /* aro del favicon mientras carga */
/* PROFUNDIDAD. La inactiva no lleva un relleno gris —un bloque sólido pesa y compite
con la activa— sino un velo casi imperceptible y una sombra INTERIOR: parece estar un
plano por debajo. Al pasar el ratón cambia de signo, la sombra sale hacia fuera y la
pestaña sube: por eso el hover va hacia el BLANCO de la activa y no hacia el gris. */
--tab-atras: rgba(0, 0, 0, 0.035);
--tab-atras-sombra: inset 0 1px 1.5px rgba(0, 0, 0, 0.055);
--tab-hover-bg: rgba(255, 255, 255, 0.85);
--tab-hover-sombra: 0 1px 3px rgba(0, 0, 0, 0.10);
--tab-activa-sombra: 0 -1px 4px rgba(0, 0, 0, 0.07);
}
/* ── 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) {
:root:not(.claro) {
--bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
--fg: #bdc1c6;
--muted: #80868b;
--tab-active-bg: #18181b; /* mismo fondo que la app → pestaña fundida con el contenido */
--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);
/* En oscuro el movimiento es el mismo pero con luz: la inactiva se hunde con un velo
blanco muy tenue y una sombra negra dentro; al pasar el ratón sube hacia la luz. */
--tab-atras: rgba(255, 255, 255, 0.035);
--tab-atras-sombra: inset 0 1px 1.5px rgba(0, 0, 0, 0.35);
--tab-hover-bg: rgba(255, 255, 255, 0.10);
--tab-hover-sombra: 0 1px 3px rgba(0, 0, 0, 0.45);
--tab-activa-sombra: 0 -1px 4px rgba(0, 0, 0, 0.35);
}
}
: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);
/* En oscuro el movimiento es el mismo pero con luz: la inactiva se hunde con un velo
blanco muy tenue y una sombra negra dentro; al pasar el ratón sube hacia la luz. */
--tab-atras: rgba(255, 255, 255, 0.035);
--tab-atras-sombra: inset 0 1px 1.5px rgba(0, 0, 0, 0.35);
--tab-hover-bg: rgba(255, 255, 255, 0.10);
--tab-hover-sombra: 0 1px 3px rgba(0, 0, 0, 0.45);
--tab-activa-sombra: 0 -1px 4px rgba(0, 0, 0, 0.35);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: var(--bg);
color: var(--fg);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 12.5px;
user-select: none;
-webkit-user-select: none;
cursor: default;
}
#bar {
display: flex;
align-items: center;
gap: 2px;
height: 40px;
padding: 0 6px 0 8px;
border-bottom: 1px solid var(--border);
}
/* macOS: hueco para los botones rojo/amarillo/verde, que son NATIVOS y se pintan encima. */
body.mac #bar { padding-left: 78px; }
#tabs {
display: flex;
align-items: flex-end;
flex: 1 1 auto;
min-width: 0;
/* Se extiende 1px sobre el borde inferior de la barra para que la pestaña ACTIVA
lo tape con su propio fondo y quede FUNDIDA con el contenido (look Chrome). */
align-self: stretch;
margin-bottom: -1px;
padding: 0 4px;
overflow: hidden;
}
.tab {
display: flex;
align-items: center;
gap: 7px;
/* 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;
min-width: 42px;
max-width: 280px;
height: 30px;
margin-bottom: 5px;
padding: 0 6px 0 10px;
border-radius: 9px; /* inactiva: píldora redondeada */
position: relative;
overflow: visible; /* las sombras viven fuera de la caja */
transition: background 140ms ease, box-shadow 140ms ease, color 140ms ease;
}
/* ── PROFUNDIDAD: las que no mandan están detrás ──────────────────────────────────
Antes la inactiva no era nada: texto suelto sobre la franja, al mismo plano que la
activa, y lo único que las separaba era un filete de 1px. Ahora la inactiva se HUNDE
—velo tenue y sombra interior— y al pasar el ratón el gesto se invierte: la sombra sale
hacia fuera y el fondo tira hacia el blanco de la activa, así que la pestaña sube hacia
ti. Es sombra y no relleno a propósito: un bloque gris sólido pesa tanto como la activa
y acaba compitiendo con ella.
Con fondo propio el separador vertical entre pestañas sobra —ya se ve dónde acaba cada
una—, así que se retiró. */
.tab:not(.active) {
background: var(--tab-atras);
box-shadow: var(--tab-atras-sombra);
}
.tab:not(.active):hover {
background: var(--tab-hover-bg);
box-shadow: var(--tab-hover-sombra);
color: var(--tab-active-fg);
}
.tab.active {
background: var(--tab-active-bg);
color: var(--tab-active-fg);
height: 35px;
margin-bottom: 0; /* su base cae 1px SOBRE el borde de la barra y lo tapa */
border-radius: 10px 10px 0 0;
/* Levantada, apenas: la sombra va HACIA ARRIBA porque abajo la pestaña está fundida con
el contenido y una sombra ahí dibujaría una línea donde justamente no debe haberla. */
box-shadow: var(--tab-activa-sombra);
z-index: 1;
}
/* AQUÍ ESTABAN LAS CURVAS CÓNCAVAS DE CHROME: dos piezas de 8px a los lados de la activa
con un cuarto de círculo transparente que derramaba su fondo hacia fuera. El truco de
Chrome funciona porque allí NO hay línea bajo la barra; aquí sí la hay, y esas dos
piezas blancas la partían en dos justo a los costados de la pestaña — la «fluctuación»
de la esquina. La activa ya se funde con el contenido por su propio fondo, que tapa la
línea exactamente por su ancho; sin las piezas no asoma nada. */
/* ── 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); }
.tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
@media (prefers-color-scheme: dark) {
:root:not(.claro) .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
: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
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. */
.tab { -webkit-user-drag: none; }
.tab.arrastrando { opacity: 0.5; }
.tab .favicon {
flex: 0 0 auto;
width: 15px;
height: 15px;
display: grid;
place-items: center;
font-size: 12.5px;
line-height: 1;
}
/* Señal de carga: el icono se sustituye por un aro girando. */
.tab .favicon.cargando {
border: 1.6px solid var(--sep);
border-top-color: var(--accent);
border-radius: 50%;
animation: giro 0.7s linear infinite;
font-size: 0;
}
@keyframes giro { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.tab .favicon.cargando { animation-duration: 2.4s; }
}
.tab .titulo {
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab .cerrar {
flex: 0 0 auto;
width: 18px;
height: 18px;
display: grid;
place-items: center;
border-radius: 50%; /* circulito, como el × de Chrome */
color: var(--muted);
font-size: 13px;
line-height: 1;
visibility: hidden;
}
.tab:hover .cerrar, .tab.active .cerrar { visibility: visible; }
.tab .cerrar:hover { background: var(--btn-hover); color: var(--tab-active-fg); }
/* Muchas pestañas abiertas: solo el icono (el nombre completo queda en el tooltip),
y la × únicamente en la activa — como hace Chrome al quedarse sin sitio. */
#tabs.compactas .tab { padding: 0 6px; justify-content: center; }
#tabs.compactas .tab .titulo { display: none; }
#tabs.compactas .tab:not(.active) .cerrar { display: none; }
button {
appearance: none;
border: 0;
background: transparent;
color: var(--fg);
font: inherit;
border-radius: 50%;
cursor: default;
display: grid;
place-items: center;
}
button:hover { background: var(--btn-hover); }
.icono {
flex: 0 0 auto;
width: 30px;
height: 26px;
font-size: 15px;
}
/* «+» de nueva pestaña, DENTRO de la tira y pegado a la última pestaña (estilo Chrome).
Hereda de `button` (transparente, redondo, hover); solo fija tamaño, evita estirarse
y se separa un pelín de la última pestaña. Bottom-alineado con las pestañas. */
.nueva-tab {
flex: 0 0 auto;
width: 28px;
height: 28px;
margin: 0 2px 6px 4px;
font-size: 18px;
line-height: 1;
}
#zoom {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 1px;
}
#z-menos, #z-mas { width: 26px; height: 26px; font-size: 14px; }
#z-pct {
min-width: 44px;
height: 26px;
padding: 0 4px;
font-variant-numeric: tabular-nums;
color: var(--muted);
font-size: 11.5px;
border-radius: 13px;
}
#z-pct.activo { color: var(--accent); font-weight: 600; }
#version {
flex: 0 0 auto;
height: 26px;
padding: 0 7px;
color: var(--muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
border-radius: 13px;
}
#version:hover { color: var(--accent); }
/* ── Botones de ventana ──────────────────────────────────────────────────────────
Solo en Windows: allí la ventana va SIN decoración, así que esta barra es la fila
del título (como Chrome) y el aspa del sistema no existe. Medidas y colores del
propio Windows 11: 46 px de ancho, alto completo de la barra, esquinas rectas
hasta el borde, y rojo #c42b1c en el aspa al pasar por encima.
Los glifos salen de la fuente de iconos del sistema (Fluent en Win 11, MDL2 en 10). */
#wincontrols { display: none; }
body.win #wincontrols {
display: flex;
flex: 0 0 auto;
align-self: stretch;
margin-left: 6px;
}
/* Los botones tienen que morder el borde derecho: la barra cede su margen. */
body.win #bar { padding-right: 0; }
#wincontrols button {
width: 46px;
border-radius: 0;
color: var(--fg);
font-family: "Segoe Fluent Icons", "Segoe MDL2 Assets", system-ui;
font-size: 10px;
line-height: 1;
}
#wincontrols button:hover { background: var(--btn-hover); }
#w-cerrar:hover { background: #c42b1c; color: #fff; }
</style>
</head>
<body>
<!-- data-tauri-drag-region: el hueco libre de la barra mueve la ventana y el doble clic
la maximiza (en macOS ya no hay barra de título nativa de la que tirar). -->
<div id="bar" data-tauri-drag-region>
<button class="icono" id="atras" title="Atrás (⌘/Ctrl+[)">←</button>
<button class="icono" id="adelante" title="Adelante (⌘/Ctrl+])">→</button>
<!-- El botón «+» de nueva pestaña ya NO va aquí fijo: se dibuja DENTRO de #tabs, pegado a
la última pestaña (estilo Chrome), en render(). Ver `.nueva-tab`. -->
<div id="tabs" data-tauri-drag-region></div>
<button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button>
<div id="zoom">
<button id="z-menos" title="Alejar (⌘/Ctrl+−)">−</button>
<button id="z-pct" title="Restablecer zoom (⌘/Ctrl+0)">100%</button>
<button id="z-mas" title="Acercar (⌘/Ctrl++)">+</button>
</div>
<button id="version" title="Versión de la app · clic: buscar actualización"></button>
<button class="icono" id="menu" title="Opciones de la app">⋮</button>
<!-- Solo visibles en Windows (clase `win` del body): allí no hay barra de título. -->
<div id="wincontrols">
<button id="w-min" title="Minimizar">&#xE921;</button>
<button id="w-max" title="Maximizar">&#xE922;</button>
<button id="w-cerrar" title="Cerrar">&#xE8BB;</button>
</div>
</div>
<script>
const T = window.__TAURI__;
const E = T && T.event;
let estado = { tabs: [], zoom: 100 };
function emitir(nombre, datos) {
try { E.emit(nombre, datos || {}); } catch (e) {}
}
const $tabs = document.getElementById("tabs");
const $pct = document.getElementById("z-pct");
const $ver = document.getElementById("version");
const $wmax = document.getElementById("w-max");
// Nombre + icono de cada pestaña a partir de su RUTA (mismas etiquetas que la barra
// superior de la web). Sin esto todas las pestañas se llamaban «(25) Labstream OS» y
// no había forma de distinguirlas.
const RUTAS = [
[/^\/$/, "🏠", "Inicio"],
[/^\/inicio/, "🏠", "Inicio"],
[/^\/mis-tareas/, "✅", "Mis tareas"],
[/^\/mis-entregas/, "📦", "Mis entregas"],
[/^\/estados/, "💬", "Chat del día"],
[/^\/chat/, "💬", "Chats"],
[/^\/notas/, "📝", "Notas"],
[/^\/recordatorios/, "⏰", "Recordatorios"],
[/^\/proyectos\/nuevo/, "✨", "Nuevo proyecto"],
[/^\/proyectos\/[^/]+/, "🎬", "Proyecto"],
[/^\/proyectos/, "🎬", "Proyectos"],
[/^\/plantillas/, "🗂️", "Plantillas"],
[/^\/calendario/, "📅", "Calendario"],
[/^\/wiki\/inventario/, "📦", "Inventario"],
[/^\/wiki\/ubicacion/, "📍", "Ubicación"],
[/^\/wiki/, "📚", "Wiki"],
[/^\/clientes\/nuevo/, "✨", "Nuevo cliente"],
[/^\/clientes\/[^/]+/, "👤", "Cliente"],
[/^\/clientes/, "👤", "Clientes"],
[/^\/operaciones/, "🗄️", "Operaciones"],
[/^\/revisiones/, "🎞️", "Revisiones"],
[/^\/entregas-finales/, "🏁", "Entregas finales"],
[/^\/cotizaciones/, "🧾", "Cotizaciones"],
[/^\/propuestas/, "📄", "Propuestas"],
[/^\/facturacion/, "💵", "Facturación"],
[/^\/comercial/, "📈", "Comercial"],
[/^\/biblioteca/, "📚", "Biblioteca"],
[/^\/reportes/, "📊", "Reportes"],
[/^\/timeline/, "🧭", "Cronograma"],
[/^\/asistente/, "🤖", "Marcebot"],
[/^\/configuracion/, "⚙️", "Configuración"],
[/^\/ajustes/, "⚙️", "Ajustes"],
[/^\/perfil/, "👤", "Mi perfil"],
[/^\/papelera/, "🗑️", "Papelera"],
[/^\/docs\//, "📄", "Documento"],
];
function rutaMeta(url) {
let ruta = "/";
try { ruta = new URL(url).pathname; } catch (e) {}
for (const [re, emoji, nombre] of RUTAS) {
if (re.test(ruta)) return { emoji, nombre };
}
return { emoji: "🗂️", nombre: "Labstream OS" };
}
// El título de la web es «(25) Labstream OS» en todas las páginas: se le quita el
// contador de no leídos y el nombre de la app. Si queda algo propio, ese manda (así,
// el día que la web ponga títulos por página, la barra los aprovecha sola).
function limpiarTitulo(t) {
if (!t) return "";
let x = String(t).replace(/^\(\d+\)\s*/, "").trim();
x = x.replace(/\s*[·|—–-]\s*Labstream(\s+OS)?\s*$/i, "").trim();
return /^labstream(\s+os)?$/i.test(x) ? "" : x;
}
function nombreDe(t) {
const meta = rutaMeta(t.url || "");
return { emoji: meta.emoji, texto: limpiarTitulo(t.title) || meta.nombre };
}
// Reordenar arrastrando, con eventos de PUNTERO: el drag&drop de HTML5 no llega al
// webview de la barra en macOS. Se mueve el nodo en vivo y al soltar se manda el orden.
let arrastre = null; // {el, x0, movido}
let tragarClick = false; // el clic que cierra el arrastre no debe activar la pestaña
function ordenActual() {
return Array.from($tabs.children).map((el) => el.dataset.label);
}
addEventListener("mousemove", (e) => {
if (!arrastre) return;
if (!arrastre.movido) {
if (Math.abs(e.clientX - arrastre.x0) < 5) return; // todavía es un clic, no un arrastre
arrastre.movido = true;
arrastre.el.classList.add("arrastrando");
}
for (const otro of Array.from($tabs.children)) {
if (otro === arrastre.el) continue;
const r = otro.getBoundingClientRect();
if (e.clientX >= r.left && e.clientX <= r.right) {
const despues = e.clientX > r.left + r.width / 2;
$tabs.insertBefore(arrastre.el, despues ? otro.nextSibling : otro);
break;
}
}
});
addEventListener("mouseup", () => {
if (!arrastre) return;
const { el, movido } = arrastre;
arrastre = null;
el.classList.remove("arrastrando");
if (movido) {
tragarClick = true;
emitir("ls-reorder", { order: ordenActual().join(",") });
}
});
// Modo solo-icono: se mide el ancho REAL de una pestaña ya pintada (el reparto lo hace
// flex, así que todas miden igual) en vez de estimarlo dividiendo el contenedor.
function ajustarCompactas() {
const primera = $tabs.firstElementChild;
if (!primera) return;
const ancho = primera.getBoundingClientRect().width;
if (!ancho) return;
$tabs.classList.toggle("compactas", ancho < 112);
}
function render() {
$tabs.textContent = "";
for (const t of estado.tabs) {
const { emoji, texto } = nombreDe(t);
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");
fav.className = "favicon" + (t.loading ? " cargando" : "");
fav.textContent = t.loading ? "" : emoji;
el.appendChild(fav);
const titulo = document.createElement("span");
titulo.className = "titulo";
titulo.textContent = texto;
el.appendChild(titulo);
const x = document.createElement("button");
x.className = "cerrar";
x.textContent = "×";
x.title = "Cerrar pestaña (⌘/Ctrl+W)";
x.addEventListener("click", (e) => {
e.stopPropagation();
emitir("ls-close-tab", { label: t.label });
});
el.appendChild(x);
el.addEventListener("click", () => {
if (tragarClick) { tragarClick = false; return; } // veníamos de reordenar
emitir("ls-activate", { label: t.label });
});
// Clic central sobre la pestaña = cerrarla (como en el navegador).
el.addEventListener("auxclick", (e) => {
if (e.button === 1) emitir("ls-close-tab", { label: t.label });
});
// Clic derecho: menú NATIVO de la pestaña (duplicar, cerrar otras, copiar enlace…).
// La barra mide 40px: un menú dibujado aquí dentro quedaría recortado.
el.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
emitir("ls-tab-menu", { label: t.label });
});
// Reordenar arrastrando: lo gestionan los listeners de puntero de más arriba.
el.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
arrastre = { el, x0: e.clientX, movido: false };
});
$tabs.appendChild(el);
}
// El «+» de nueva pestaña, PEGADO a la última (estilo Chrome): al vivir dentro de #tabs
// como último elemento (flex 0 0 auto), queda justo tras la última pestaña en vez de
// fijo a la derecha de la barra. El hueco libre después de él sigue moviendo la ventana.
const mas = document.createElement("button");
mas.className = "nueva-tab";
mas.textContent = "+";
mas.title = "Nueva pestaña (⌘/Ctrl+T)";
mas.addEventListener("click", () => emitir("ls-new-tab", {}));
$tabs.appendChild(mas);
requestAnimationFrame(ajustarCompactas);
const activa = $tabs.querySelector(".tab.active");
if (activa) activa.scrollIntoView({ block: "nearest", inline: "nearest" });
$pct.textContent = estado.zoom + "%";
$pct.classList.toggle("activo", estado.zoom !== 100);
$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.
document.body.classList.toggle("mac", !!estado.mac);
// En Windows también ES la fila del título, pero ahí los botones de ventana los
// pinta esta barra: el del medio alterna entre maximizar (▢) y restaurar (❐).
document.body.classList.toggle("win", !!estado.win);
if (estado.win) {
const max = !!estado.maximized;
$wmax.textContent = max ? "\uE923" : "\uE922"; // ChromeRestore / ChromeMaximize
$wmax.title = max ? "Restaurar" : "Maximizar";
}
}
if (E) {
E.listen("ls-tabs", (ev) => {
estado = ev.payload || estado;
render();
});
}
// Al cambiar el ancho de la ventana se recalcula si caben los nombres.
addEventListener("resize", ajustarCompactas);
document.getElementById("atras").addEventListener("click", () => emitir("ls-nav", { dir: -1 }));
document.getElementById("adelante").addEventListener("click", () => emitir("ls-nav", { dir: 1 }));
document.getElementById("recargar").addEventListener("click", () => emitir("ls-reload", {}));
// Menú de OPCIONES de la app (nativo, lo pinta Rust): botón ⋮ o clic derecho en la barra.
document.getElementById("menu").addEventListener("click", () => emitir("ls-menu", {}));
$ver.addEventListener("click", () => emitir("ls-check-update", {}));
document.getElementById("bar").addEventListener("contextmenu", (e) => {
e.preventDefault();
emitir("ls-menu", {});
});
document.getElementById("z-menos").addEventListener("click", () => emitir("ls-zoom", { dir: -1 }));
document.getElementById("z-mas").addEventListener("click", () => emitir("ls-zoom", { dir: 1 }));
$pct.addEventListener("click", () => emitir("ls-zoom", { dir: 0 }));
// Botones de ventana (Windows). Cerrar oculta a la bandeja, como el aspa de siempre.
document.getElementById("w-min").addEventListener("click", () => emitir("ls-window", { act: "min" }));
$wmax.addEventListener("click", () => emitir("ls-window", { act: "max" }));
document.getElementById("w-cerrar").addEventListener("click", () => emitir("ls-window", { act: "close" }));
// El doble clic en el hueco libre lo gobierna `data-tauri-drag-region`: maximiza y
// restaura, como la barra de título del sistema y como la de pestañas de Chrome.
// (Antes abría una pestaña nueva; ahora que la barra ES el título, chocaba con él.
// Para una pestaña nueva quedan el botón + y ⌘/Ctrl+T.)
// Atajos también cuando el foco está en la barra.
addEventListener("keydown", (e) => {
const mod = e.metaKey || e.ctrlKey;
if (!mod) return;
const k = e.key;
if (k === "t" || k === "T") { e.preventDefault(); emitir(e.shiftKey ? "ls-reopen" : "ls-new-tab", {}); }
else if (k === "w" || k === "W") { e.preventDefault(); emitir("ls-close-tab", {}); }
else if ((k === "r" || k === "R") && !e.shiftKey) { e.preventDefault(); emitir("ls-reload", {}); }
else if (k === "[") { e.preventDefault(); emitir("ls-nav", { dir: -1 }); }
else if (k === "]") { e.preventDefault(); emitir("ls-nav", { dir: 1 }); }
else if (k === "=" || k === "+") { e.preventDefault(); emitir("ls-zoom", { dir: 1 }); }
else if (k === "-") { e.preventDefault(); emitir("ls-zoom", { dir: -1 }); }
else if (k === "0") { e.preventDefault(); emitir("ls-zoom", { dir: 0 }); }
else if (k === "Tab") { e.preventDefault(); emitir("ls-cycle", { dir: e.shiftKey ? -1 : 1 }); }
else if (k >= "1" && k <= "9") { e.preventDefault(); emitir("ls-activate-n", { n: +k }); }
});
emitir("ls-ready", {});
</script>
</body>
</html>