Compare commits

...

7 Commits

Author SHA1 Message Date
Carlos Narro
95d59acc91 CLAUDE.md: alta como proyecto — especialidad desarrollo de la extensión MV3
Manual operativo del proyecto: arquitectura (content.js + use.html snapshot),
cómo cargar/probar sin build, y gotchas (fragilidad ante cambios de claude.ai,
Boost 2x caduco, supuesto semanal-lunes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 13:54:07 +02:00
Carlos Narro
699a8566d0 Simplificar a "FAB abre modal nativo" y añadir hora fin sesión
El usuario prefiere ver la página de Uso nativa de Claude en vez de un
overlay propio. Eso permite borrar mucho código:
- Fuera openOverlayPanel, closeOverlayPanel, setOverlaySpinner,
  renderOverlayWithCache, showOverlayError.
- Fuera refreshInBackground, tryOpenViaPushState/Hash/Click,
  closeUsageModal, waitForDialogWithData — ya no auto-abrimos en
  background.
- Fuera formatAge y CACHE_TTL_MS y OVERLAY_ID y refreshPromise.
- Fuera __claudeUsageTracker (era andamio de debug).
- Fuera bloque de CSS del overlay (~120 líneas).

Nuevo onFabClick: 3 intentos en cascada para abrir el modal nativo de
Claude (hash → pushState+popstate → click en "Uso"). Si los 3 fallan,
warn en consola y queda el manual.

Y siguiendo lo que pediste, en el resumen del tracker (el que se
inyecta dentro del modal) ahora aparece un bloque azul "⏱ Sesión
termina a las HH:MM (en Xh Ym)" justo debajo del bloque verde del
reinicio semanal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:04:46 +02:00
Carlos Narro
8b6c9dac59 FAB miniatura: mostrar usado/ideal sesión y semanal
Antes la miniatura mostraba solo "X% / Y%" (sesión / semanal). Ahora
muestra "sUsado%/sIdeal% ~ wUsado%/wIdeal%" para ver de un vistazo si
vas por encima o por debajo del ritmo proporcional al tiempo
transcurrido en la sesión y en la semana.

- sIdeal = % de los 5h de sesión ya transcurridos.
- wIdeal = idealUsedByNow (% de la semana ya transcurrida).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:00:30 +02:00
Carlos Narro
5ba8a7437a Defensa en createWeeklyTracker: normalizar Date in situ
El error TypeError: estimatedEndDate.toLocaleDateString seguía
ocurriendo a pesar de rehydrateDates al cargar caché. Sin entender
todavía por qué la rehidratación no convierte el valor (posible que
el campo en storage no sea ISO-string sino algo no contemplado),
añado defense in depth: createWeeklyTracker normaliza los tres campos
Date (sessionStart, sessionEnd, estimatedEndDate) en su primera línea.

Es no-op cuando rehydrateDates funciona (instanceof Date → as-is) y
rescata el render cuando falla por cualquier motivo. Una llamada a
toLocaleDateString sobre un string ya no rompe la extensión.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 15:47:03 +02:00
Carlos Narro
6e5f589156 Rehidratar Date al cargar caché y refresh no-destructivo
Bug raíz del click-no-abre-overlay: chrome.storage serializa con JSON,
los Date (sessionStartTime, sessionEndTime, estimatedEndDate) volvían
como strings ISO, y createWeeklyTracker llamaba .toLocaleDateString
en un string. rehydrateDates los devuelve a Date al cargar.

Mejoras adicionales:
- showOverlayError ya no destruye el panel si hay tracker visible:
  saca un aviso temporal en el header (auto-oculta a los 7s).
- DOM en el aviso construido con textContent, no innerHTML, para
  cerrar la puerta a XSS aunque el msg sea hardcodeado.
- Limpieza del andamio [CUT-DIAG] usado para encontrar el bug;
  __claudeUsageTracker se queda expuesto para depuración futura.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:45:00 +02:00
Carlos Narro
d2bc177376 Instrumentar FAB→overlay: logs [CUT-DIAG] y __claudeUsageTracker
El FAB se ve y muestra datos cacheados (47%/40%) pero el click no abre
el overlay. Añado:
- console.log en cada paso del camino click→cache→openOverlayPanel
- try/catch en onFabClick y openOverlayPanel para cazar fallos silenciosos
- pointerdown listener en FAB para diferenciar "click cancelado por
  Claude" de "handler nunca conectado"
- window.__claudeUsageTracker con loadCache, openOverlayPanel,
  forceShowOverlay, etc. para reproducir cada paso desde la consola

Cambios temporales: una vez identificada la causa se retiran.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:38:32 +02:00
Carlos Narro
673bd0bced Adaptar tracker al nuevo modal de Uso
Claude movió la página de uso de /settings/usage a un modal con hash
#settings/usage sobre cualquier URL, así que el content script no se
ejecutaba en ningún sitio. Tres flujos nuevos:

- FAB flotante en claude.ai/* con uso cacheado (X% / Y%); se oculta
  mientras el modal real está abierto.
- Inyección del tracker dentro del modal real cuando el usuario lo abre,
  anclando estructuralmente sobre la fila del primer progressbar
  (locale-proof) en vez de por texto.
- Overlay propio abierto desde el FAB con caché instantánea y refresh
  en background si TTL >5 min: intenta pushState+popstate, hash,
  click en "Uso" como fallbacks; cierra con botón X o Escape.

Cache en chrome.storage.local; se calienta cuando el usuario abre el
modal por su cuenta. Observers acotados y desconectados agresivamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 13:57:11 +02:00
4 changed files with 409 additions and 69 deletions

40
CLAUDE.md Normal file
View File

@@ -0,0 +1,40 @@
# Claude Usage Tracker — extensión de Chrome (MV3)
Extensión de navegador (Manifest V3) que **mejora la página de uso de Claude** (`https://claude.ai/settings/usage`): parte la barra de cuota semanal en los 7 días, muestra info de sesión, y añade un FAB con el uso usado/ideal. Todo local en el navegador; no manda nada a ningún servidor.
**Objetivo de este proyecto**: iterar sobre la extensión para mejorarla. No hay build ni servidor — es JS/CSS vanilla que se **carga sin empaquetar** en Chrome/Edge y actúa como *content script* sobre claude.ai.
## Tu rol
Eres experto en **extensiones Chrome MV3** y en la estructura del DOM de la página de uso de claude.ai. Mejora la extensión con cambios mínimos y probados. No inventes un pipeline: aquí no hay npm, ni bundler, ni tests automatizados — se edita el fichero y se recarga en el navegador.
## Ficheros
| Fichero | Qué es |
|---|---|
| `manifest.json` | MV3. `permissions: ["storage"]`, `content_scripts` con `matches: ["https://claude.ai/*"]`, `run_at: document_idle`, inyecta `content.js` + `styles.css`. **No** declara iconos ni `action`/popup ni `background` → es puramente content script |
| `content.js` (~38 KB) | El motor. Escanea el DOM de la página de uso, extrae el % de uso, dibuja el tracker semanal (7 días, verde/amarillo/rojo), la info de sesión (inicio/actual/duración) y el FAB miniatura (usado/ideal de sesión y semanal). Sesión guardada en `localStorage` (`claude_usage_tracker_session`, ventana de 4 h). Incluye una config de promo **"Boost 2×"** con fechas *hardcodeadas* (marzo 2026, peak hours ET) |
| `styles.css` (~11 KB) | Estilos del tracker, FAB y modal |
| `use.html` (~82 KB) | **Snapshot** de la página real de uso (`data-org-plan="claude_max"`, dark). Es el DOM que `content.js` debe parsear → úsalo como *fixture* para razonar sobre selectores sin depender de la web viva |
| `icons/icon.svg` | Icono fuente. **No hacen falta PNGs** (el manifest v3 no referencia iconos; el README los pedía pero está obsoleto) |
| `README.md` | Descripción de features + instalación (ojo: el paso "crear iconos PNG" está desfasado) |
## Cómo desarrollar y probar
1. Editar `content.js` / `styles.css`.
2. Cargar sin empaquetar (una vez): `chrome://extensions`**Modo de desarrollador****Cargar extensión sin empaquetar** → esta carpeta.
3. Tras cada cambio: botón **↻ recargar** de la extensión en `chrome://extensions` **y** recargar la pestaña `https://claude.ai/settings/usage`.
4. Para iterar sobre la lógica de parseo sin la web viva, abrir `use.html` en el navegador y probar contra ese DOM.
"Instalar" (cuando esté lista) = exactamente el paso 2. No hay despliegue web.
## Gotchas
- **Frágil ante cambios de claude.ai**: los selectores de `content.js` dependen de la estructura real de `claude.ai/settings/usage`, que Anthropic cambia sin avisar. Si el tracker deja de pintar, lo primero es re-inspeccionar el DOM (y el `use.html` snapshot puede haber quedado desfasado → recapturarlo).
- **Config "Boost 2×" caduca**: fechas de marzo 2026 hardcodeadas en `content.js` (`BOOST_END_DATE`, `PEAK_START_UTC`…). Revisar si sigue vigente o generalizar.
- **Supuestos del cálculo**: asume reinicio semanal los **lunes** y uso ideal 100%/7 ≈ **14.29%/día**. El ciclo real de cuota de Anthropic puede no ser semanal-lunes; verificar antes de fiarse de los colores.
- **Privacidad**: los datos de sesión viven solo en `localStorage`; nada sale a servidores. Mantenerlo así.
## Git
Repo privado en Gitea: `https://git.carlosnarro.com/carlos/claude-use-extension` (rama `main`). No hay CI/deploy — commit + push y ya.

View File

@@ -492,6 +492,22 @@
} }
function createWeeklyTracker(pageData, dailyData) { function createWeeklyTracker(pageData, dailyData) {
// Defense in depth: si pageData/dailyData vienen del caché de chrome.storage,
// los Date pueden seguir siendo string/number aunque rehydrateDates haya pasado.
// Normalizar aquí mismo evita TypeError al llamar .toLocaleDateString/Time.
const ensureDate = (v) => {
if (v == null) return null;
if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
if (typeof v === 'string' || typeof v === 'number') {
const d = new Date(v);
return isNaN(d.getTime()) ? null : d;
}
return null;
};
pageData.sessionStartTime = ensureDate(pageData.sessionStartTime);
pageData.sessionEndTime = ensureDate(pageData.sessionEndTime);
dailyData.estimatedEndDate = ensureDate(dailyData.estimatedEndDate);
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'claude-usage-tracker-container'; container.className = 'claude-usage-tracker-container';
container.id = 'claude-usage-tracker'; container.id = 'claude-usage-tracker';
@@ -571,6 +587,14 @@
${dailyData.willRunOut ? `<span class="estimated-end-hours">(en ~${Math.round(dailyData.hoursToReach100)}h)</span>` : ''} ${dailyData.willRunOut ? `<span class="estimated-end-hours">(en ~${Math.round(dailyData.hoursToReach100)}h)</span>` : ''}
</div> </div>
${pageData.sessionEndTime ? `
<div class="session-reset-info">
<span class="session-reset-info-label">⏱ Sesión termina</span>
<span class="session-reset-info-time">a las ${formatTime(pageData.sessionEndTime)}</span>
<span class="session-reset-info-remaining">(en ${Math.floor(pageData.sessionTimeRemainingMin / 60)}h ${pageData.sessionTimeRemainingMin % 60}m)</span>
</div>
` : ''}
<div class="usage-info-row"> <div class="usage-info-row">
<div class="usage-stat"> <div class="usage-stat">
<span class="usage-stat-label">Tiempo transcurrido</span> <span class="usage-stat-label">Tiempo transcurrido</span>
@@ -644,93 +668,289 @@
return container; return container;
} }
function injectTracker() { // ===========================================================================
// Remove existing tracker if present // Bootstrap nuevo (modal de Uso ahora es un <div role="dialog"> con hash
const existing = document.getElementById('claude-usage-tracker'); // #settings/usage sobre cualquier URL). Tres flujos:
if (existing) { // 1) Botón flotante (FAB) en todas las páginas de claude.ai.
existing.remove(); // 2) Inyección del tracker dentro del modal real cuando el usuario lo abre.
// 3) Overlay propio abierto desde el FAB, con caché y auto-refresh.
// ===========================================================================
const CACHE_KEY = 'claude_usage_cache';
const FAB_ID = 'claude-usage-tracker-fab';
const TRACKER_ID = 'claude-usage-tracker';
let dialogObserver = null;
// -------- Cache (chrome.storage.local) --------
// chrome.storage serializa con JSON: los Date salen como strings ISO. Al
// cargar, rehidratamos los campos conocidos para que createWeeklyTracker
// pueda llamar toLocaleDateString/Time sin TypeError.
function rehydrateDates(cache) {
if (!cache) return null;
const toDate = (v) => {
if (v == null) return null;
if (v instanceof Date) return v;
if (typeof v === 'string') {
const d = new Date(v);
return isNaN(d.getTime()) ? null : d;
}
return v;
};
if (cache.pageData) {
cache.pageData.sessionStartTime = toDate(cache.pageData.sessionStartTime);
cache.pageData.sessionEndTime = toDate(cache.pageData.sessionEndTime);
}
if (cache.dailyData) {
cache.dailyData.estimatedEndDate = toDate(cache.dailyData.estimatedEndDate);
}
return cache;
}
function loadCache() {
return new Promise(resolve => {
try {
chrome.storage.local.get([CACHE_KEY], result => resolve(rehydrateDates(result[CACHE_KEY] || null)));
} catch (e) {
resolve(null);
}
});
}
function saveCache(pageData, dailyData, source) {
try {
chrome.storage.local.set({
[CACHE_KEY]: {
pageData,
dailyData,
scrapedAt: new Date().toISOString(),
source: source || 'auto'
}
});
} catch (e) {
console.warn('[Claude Usage Tracker] No se pudo guardar caché:', e);
}
}
function cacheAgeMin(scrapedAt) {
if (!scrapedAt) return Infinity;
return (Date.now() - new Date(scrapedAt).getTime()) / 60000;
}
// -------- Detección del modal de Uso --------
function findUsageDialog() {
const dialogs = document.querySelectorAll('[role="dialog"]');
for (const d of dialogs) {
const txt = d.innerText || '';
if (txt.includes('Todos los modelos') || txt.includes('Sesión actual')) {
return d;
}
}
return null;
}
function isUsageDialogOpen() {
return !!findUsageDialog();
}
function dialogHasData(dialog) {
if (!dialog) return false;
return !!dialog.querySelector('[role="progressbar"][aria-valuenow]');
}
// -------- Inyección dentro del modal real --------
function findDialogInjectionAnchor(dialog) {
// Estructural, locale-proof: primer progressbar → subir al ancestro que
// también contiene la etiqueta "Todos los modelos" o "Sesión actual" — esa
// es la fila de la primera métrica, y prepend antes de ella.
const firstPb = dialog.querySelector('[role="progressbar"]');
if (!firstPb) return null;
let row = firstPb;
for (let i = 0; i < 12 && row; i++) {
const txt = row.innerText || '';
if (txt.includes('Todos los modelos') || txt.includes('Sesión actual')) {
return row;
}
row = row.parentElement;
}
return firstPb.parentElement;
}
function injectTrackerInDialog(dialog) {
if (!dialog) return false;
// Idempotencia: si ya hay un tracker DENTRO de este dialog, no duplicar.
if (dialog.querySelector('#' + TRACKER_ID)) return true;
const pageData = parsePageData();
if (pageData.weeklyUsage === 0 && pageData.resetDayIndex === -1) {
console.log('[Claude Usage Tracker] Modal abierto pero sin datos legibles aún');
return false;
} }
initSession(); initSession();
// Parsear datos reales de la página
const pageData = parsePageData();
// Si no encontramos datos, no inyectar nada
if (pageData.weeklyUsage === 0 && pageData.resetDayIndex === -1) {
console.log('[Claude Usage Tracker] No se encontraron datos de uso en la página');
return;
}
const dailyData = calculateDailyUsage(pageData); const dailyData = calculateDailyUsage(pageData);
const tracker = createWeeklyTracker(pageData, dailyData); const tracker = createWeeklyTracker(pageData, dailyData);
// Insertar justo DEBAJO del título principal h1 const anchor = findDialogInjectionAnchor(dialog);
const mainContent = document.querySelector('main'); if (anchor && anchor.parentElement) {
if (!mainContent) { anchor.parentElement.prepend(tracker);
console.log('[Claude Usage Tracker] No se encontró el main');
return;
}
// Buscar el h1 del título principal "Ajustes" (visible en móvil)
const mobileH1 = mainContent.querySelector('h1.font-heading');
if (mobileH1) {
// Insertar justo después del h1
mobileH1.after(tracker);
} else { } else {
// Fallback: insertar al principio del main content dialog.prepend(tracker);
const contentDiv = mainContent.querySelector('.pb-8') || mainContent.querySelector('div > div');
if (contentDiv) {
contentDiv.prepend(tracker);
} else {
mainContent.prepend(tracker);
}
} }
console.log('[Claude Usage Tracker] Inyectado en la parte superior', { saveCache(pageData, dailyData, 'manual');
pageData, updateFabState({ pageData, dailyData, scrapedAt: new Date().toISOString() });
dailyData
}); console.log('[Claude Usage Tracker] Inyectado dentro del modal', { pageData });
return true;
} }
// Wait for page to fully load // -------- Observers --------
function waitForContent() { function observeDialogForData(dialog, onReady) {
const observer = new MutationObserver((mutations, obs) => { if (dialogHasData(dialog)) { onReady(); return; }
const main = document.querySelector('main'); const obs = new MutationObserver(() => {
const hasContent = main && main.textContent.length > 100; if (dialogHasData(dialog)) {
if (hasContent) {
obs.disconnect(); obs.disconnect();
setTimeout(injectTracker, 500); // Small delay to ensure content is rendered onReady();
} }
}); });
obs.observe(dialog, {
observer.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true,
attributes: true,
attributeFilter: ['aria-valuenow']
}); });
setTimeout(() => { obs.disconnect(); onReady(); }, 4000);
// Timeout fallback
setTimeout(() => {
observer.disconnect();
injectTracker();
}, 3000);
} }
// Initialize function startGlobalDialogWatcher() {
if (document.readyState === 'loading') { if (dialogObserver) return;
document.addEventListener('DOMContentLoaded', waitForContent); let lastDialog = null;
} else { dialogObserver = new MutationObserver(() => {
waitForContent(); const dialog = findUsageDialog();
if (dialog && dialog !== lastDialog) {
lastDialog = dialog;
observeDialogForData(dialog, () => injectTrackerInDialog(dialog));
} else if (!dialog) {
lastDialog = null;
}
updateFabVisibility();
});
dialogObserver.observe(document.body, { childList: true, subtree: true });
} }
// Update session time periodically // -------- FAB --------
setInterval(() => { function injectFloatingButton() {
const sessionValue = document.querySelector('.session-info-value'); if (document.getElementById(FAB_ID)) return;
if (sessionValue) { const fab = document.createElement('button');
injectTracker(); fab.id = FAB_ID;
fab.className = 'claude-usage-tracker-fab';
fab.title = 'Claude Usage Tracker';
fab.innerHTML = `
<span class="claude-usage-tracker-fab-icon">📊</span>
<span class="claude-usage-tracker-fab-text">…</span>
`;
fab.addEventListener('click', onFabClick);
document.body.appendChild(fab);
loadCache().then(updateFabState);
}
function updateFabState(cache) {
const fab = document.getElementById(FAB_ID);
if (!fab) return;
const textEl = fab.querySelector('.claude-usage-tracker-fab-text');
if (!textEl) return;
if (!cache || !cache.pageData) { textEl.textContent = '—'; return; }
const pd = cache.pageData;
const dd = cache.dailyData || {};
const sessionTotalMin = SESSION_DURATION_HOURS * 60;
// usado e ideal de SESIÓN
const sUsed = Math.round(pd.sessionUsage || 0);
const sIdeal = Math.max(0, Math.min(100,
Math.round(100 - ((pd.sessionTimeRemainingMin || 0) / sessionTotalMin) * 100)));
// usado e ideal SEMANAL
const wUsed = Math.round(pd.weeklyUsage || 0);
const wIdeal = Math.round(dd.idealUsedByNow || 0);
textEl.textContent = `${sUsed}%/${sIdeal}% ~ ${wUsed}%/${wIdeal}%`;
}
function updateFabVisibility() {
const fab = document.getElementById(FAB_ID);
if (!fab) return;
if (isUsageDialogOpen()) fab.classList.add('claude-usage-tracker-fab--hidden');
else fab.classList.remove('claude-usage-tracker-fab--hidden');
}
// Click en el FAB: abrir el modal de Uso de Claude. Intenta varios métodos
// por si el router no escucha hashchange (Next.js puede colgar de popstate o
// de clicks en sus propios links). El usuario se conforma con ver el panel
// nativo, así que no abrimos nada propio.
async function onFabClick() {
try {
if (isUsageDialogOpen()) return;
// Método 1: setear hash
if (location.hash !== '#settings/usage') {
location.hash = '#settings/usage';
} else {
// ya estaba seteado pero el modal no está abierto: limpiar primero
location.hash = '';
await new Promise(r => setTimeout(r, 100));
location.hash = '#settings/usage';
}
await new Promise(r => setTimeout(r, 600));
if (isUsageDialogOpen()) return;
// Método 2: pushState + popstate
history.pushState(null, '', '#settings/usage');
window.dispatchEvent(new PopStateEvent('popstate'));
await new Promise(r => setTimeout(r, 600));
if (isUsageDialogOpen()) return;
// Método 3: clickar el enlace "Uso" si está visible
const usoBtn = [...document.querySelectorAll('a, button')]
.find(el => (el.innerText || '').trim() === 'Uso' && el.offsetParent !== null);
if (usoBtn) {
usoBtn.click();
return;
}
console.warn('[Claude Usage Tracker] No he podido abrir el modal de Uso. Ábrelo manualmente desde el avatar.');
} catch (e) {
console.error('[Claude Usage Tracker] FAB click error:', e);
} }
}, 60000); // Update every minute }
// -------- Bootstrap --------
function bootstrap() {
injectFloatingButton();
startGlobalDialogWatcher();
window.addEventListener('hashchange', () => {
updateFabVisibility();
const dialog = findUsageDialog();
if (dialog) observeDialogForData(dialog, () => injectTrackerInDialog(dialog));
});
window.addEventListener('popstate', updateFabVisibility);
// Sanity check: re-inyectar FAB si React lo eliminó
setInterval(() => {
injectFloatingButton();
updateFabVisibility();
}, 2000);
// Si el modal ya estaba abierto al cargar
setTimeout(() => {
const dialog = findUsageDialog();
if (dialog) observeDialogForData(dialog, () => injectTrackerInDialog(dialog));
updateFabVisibility();
}, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})(); })();

View File

@@ -6,7 +6,7 @@
"permissions": ["storage"], "permissions": ["storage"],
"content_scripts": [ "content_scripts": [
{ {
"matches": ["https://claude.ai/settings/usage*"], "matches": ["https://claude.ai/*"],
"js": ["content.js"], "js": ["content.js"],
"css": ["styles.css"], "css": ["styles.css"],
"run_at": "document_idle" "run_at": "document_idle"

View File

@@ -524,3 +524,83 @@
color: var(--text-300, #a3a3a3); color: var(--text-300, #a3a3a3);
font-size: 12px; font-size: 12px;
} }
/* ===========================================================================
Floating button (FAB) — visible en todas las páginas de claude.ai
=========================================================================== */
.claude-usage-tracker-fab {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999998;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: rgba(20, 20, 22, 0.92);
color: #f5f5f5;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 999px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: transform 0.15s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
.claude-usage-tracker-fab:hover {
transform: translateY(-1px);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55);
border-color: rgba(255, 255, 255, 0.22);
}
.claude-usage-tracker-fab:active {
transform: translateY(0);
}
.claude-usage-tracker-fab--hidden {
display: none !important;
}
.claude-usage-tracker-fab-icon {
font-size: 16px;
line-height: 1;
}
.claude-usage-tracker-fab-text {
font-variant-numeric: tabular-nums;
letter-spacing: 0.01em;
}
/* ===========================================================================
Resumen — hora del próximo reinicio de sesión
=========================================================================== */
.session-reset-info {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
padding: 10px 14px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 8px;
font-size: 13px;
}
.session-reset-info-label {
color: var(--text-300, #a3a3a3);
}
.session-reset-info-time {
font-weight: 600;
color: var(--text-100, #f5f5f5);
}
.session-reset-info-remaining {
color: var(--text-200, #d4d4d4);
font-weight: 500;
margin-left: auto;
}