Con la barra ya en la fila del título, teñir es lo que hace que un montón de pestañas abiertas se lea de un vistazo: todo lo de un cliente en verde, lo que está en revisión en rojo. Se elige con clic derecho sobre la pestaña ▸ Color, con marca en el que tiene puesto, y "Sin color" para quitarlo. Nueve colores, los mismos que usa Chrome para sus grupos. El color se guarda con la sesión (lista `colors` en estado.json, paralela a `tabs`), así que sobrevive a cerrar la app. Un estado.json viejo sin colores carga igual: la lista entra por serde default y todas las pestañas salen sin teñir. Cómo se pinta: la inactiva lleva el color de fondo translúcido y se le quita el separador vertical (con fondo propio ya se ve dónde acaba). La ACTIVA no se tiñe entera —tiene que seguir fundida con el contenido, que es lo que le da el aspecto de Chrome— sino que lleva el color en una franja de 3 px arriba. En el CSS el color 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, en vez de repetir nueve colores por tema. Al restaurar la sesión, el color se empareja con su URL ANTES de filtrar por origen: el filtro puede descartar entradas y entonces los índices de las dos listas dejarían de casar, y las pestañas saldrían con el color de otra. El id que llega del menú se valida contra la paleta antes de guardarlo; uno desconocido deja la pestaña sin color en vez de escribir cualquier cosa en el disco. Verificado: sintaxis del script con node --check, y en el navegador que la inactiva se tiñe, que la activa conserva su fondo de tarjeta con la franja del color encima, y que el separador desaparece en las teñidas. La compilación la verifica el CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
566 líneas
25 KiB
HTML
566 líneas
25 KiB
HTML
<!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}], zoom, version, mac}
|
||
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>
|
||
/* Paleta estilo Chrome: la barra es una FRANJA más oscura y la pestaña activa es una
|
||
tarjeta del color del contenido que se funde con él (sin línea debajo). */
|
||
:root {
|
||
--bg: #dee1e6; /* franja de pestañas (Chrome claro) */
|
||
--fg: #45474a;
|
||
--muted: #5f6368;
|
||
--tab-active-bg: #ffffff;
|
||
--tab-active-fg: #202124;
|
||
--accent: #6366f1; /* indigo-500, color de Labstream */
|
||
--border: #c9ccd1; /* 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 */
|
||
--btn-hover: rgba(0, 0, 0, 0.08);
|
||
--sep: rgba(0, 0, 0, 0.22); /* separador fino entre pestañas inactivas */
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--bg: #202124; /* franja (Chrome oscuro) */
|
||
--fg: #bdc1c6;
|
||
--muted: #80868b;
|
||
--tab-active-bg: #18181b; /* mismo fondo que la app → pestaña fundida con el contenido */
|
||
--tab-active-fg: #e8eaed;
|
||
--border: #101013;
|
||
--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; }
|
||
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. */
|
||
flex: 1 1 220px;
|
||
min-width: 42px;
|
||
max-width: 240px;
|
||
height: 30px;
|
||
margin-bottom: 5px;
|
||
padding: 0 6px 0 10px;
|
||
border-radius: 9px; /* inactiva: píldora redondeada (hover) */
|
||
position: relative;
|
||
overflow: visible; /* deja asomar las curvas de la base de la activa */
|
||
}
|
||
.tab:not(.active):hover { background: var(--hover); }
|
||
/* Separador fino a la IZQUIERDA de cada inactiva; se oculta pegado a la activa,
|
||
al hover y en la primera (igual que Chrome). */
|
||
.tab:not(.active)::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: -1px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 1px;
|
||
height: 16px;
|
||
background: var(--sep);
|
||
}
|
||
.tab:first-child::before,
|
||
.tab:hover::before,
|
||
.tab.active + .tab::before,
|
||
.tab:hover + .tab::before { display: none; }
|
||
.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;
|
||
z-index: 1;
|
||
}
|
||
/* Curvas cóncavas de Chrome en la base de la pestaña activa: dos piezas de 8px con
|
||
un cuarto de círculo transparente que "derrama" el fondo hacia los lados. */
|
||
.tab.active::before,
|
||
.tab.active::after {
|
||
content: "";
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
.tab.active::before {
|
||
left: -8px;
|
||
background: radial-gradient(circle at 0 0, transparent 8px, var(--tab-active-bg) 8.5px);
|
||
}
|
||
.tab.active::after {
|
||
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. */
|
||
.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;
|
||
}
|
||
#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>
|
||
<div id="tabs" data-tauri-drag-region></div>
|
||
<button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button>
|
||
<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"></button>
|
||
<button id="w-max" title="Maximizar"></button>
|
||
<button id="w-cerrar" title="Cerrar"></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);
|
||
}
|
||
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 : "";
|
||
// 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("nueva").addEventListener("click", () => emitir("ls-new-tab", {}));
|
||
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>
|