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>
This commit is contained in:
465
content.js
465
content.js
@@ -644,93 +644,420 @@
|
||||
return container;
|
||||
}
|
||||
|
||||
function injectTracker() {
|
||||
// Remove existing tracker if present
|
||||
const existing = document.getElementById('claude-usage-tracker');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
// ===========================================================================
|
||||
// Bootstrap nuevo (modal de Uso ahora es un <div role="dialog"> con hash
|
||||
// #settings/usage sobre cualquier URL). Tres flujos:
|
||||
// 1) Botón flotante (FAB) en todas las páginas de claude.ai.
|
||||
// 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 CACHE_TTL_MS = 5 * 60 * 1000; // 5 min
|
||||
const FAB_ID = 'claude-usage-tracker-fab';
|
||||
const OVERLAY_ID = 'claude-usage-tracker-overlay';
|
||||
const TRACKER_ID = 'claude-usage-tracker';
|
||||
|
||||
let dialogObserver = null;
|
||||
let refreshPromise = null;
|
||||
|
||||
// -------- Cache (chrome.storage.local) --------
|
||||
function loadCache() {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
chrome.storage.local.get([CACHE_KEY], result => resolve(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;
|
||||
}
|
||||
|
||||
function formatAge(min) {
|
||||
if (!isFinite(min)) return 'sin datos';
|
||||
if (min < 1) return 'ahora';
|
||||
if (min < 60) return `hace ${Math.floor(min)} min`;
|
||||
const h = Math.floor(min / 60);
|
||||
const m = Math.floor(min % 60);
|
||||
return `hace ${h}h ${m}m`;
|
||||
}
|
||||
|
||||
// -------- 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();
|
||||
|
||||
// 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 tracker = createWeeklyTracker(pageData, dailyData);
|
||||
|
||||
// Insertar justo DEBAJO del título principal h1
|
||||
const mainContent = document.querySelector('main');
|
||||
if (!mainContent) {
|
||||
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);
|
||||
|
||||
const anchor = findDialogInjectionAnchor(dialog);
|
||||
if (anchor && anchor.parentElement) {
|
||||
anchor.parentElement.prepend(tracker);
|
||||
} else {
|
||||
// Fallback: insertar al principio del main content
|
||||
const contentDiv = mainContent.querySelector('.pb-8') || mainContent.querySelector('div > div');
|
||||
if (contentDiv) {
|
||||
contentDiv.prepend(tracker);
|
||||
} else {
|
||||
mainContent.prepend(tracker);
|
||||
}
|
||||
dialog.prepend(tracker);
|
||||
}
|
||||
|
||||
console.log('[Claude Usage Tracker] Inyectado en la parte superior', {
|
||||
pageData,
|
||||
dailyData
|
||||
});
|
||||
saveCache(pageData, dailyData, 'manual');
|
||||
updateFabState({ pageData, dailyData, scrapedAt: new Date().toISOString() });
|
||||
|
||||
console.log('[Claude Usage Tracker] Inyectado dentro del modal', { pageData });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait for page to fully load
|
||||
function waitForContent() {
|
||||
const observer = new MutationObserver((mutations, obs) => {
|
||||
const main = document.querySelector('main');
|
||||
const hasContent = main && main.textContent.length > 100;
|
||||
|
||||
if (hasContent) {
|
||||
// -------- Observers --------
|
||||
function observeDialogForData(dialog, onReady) {
|
||||
if (dialogHasData(dialog)) { onReady(); return; }
|
||||
const obs = new MutationObserver(() => {
|
||||
if (dialogHasData(dialog)) {
|
||||
obs.disconnect();
|
||||
setTimeout(injectTracker, 500); // Small delay to ensure content is rendered
|
||||
onReady();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
obs.observe(dialog, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['aria-valuenow']
|
||||
});
|
||||
setTimeout(() => { obs.disconnect(); onReady(); }, 4000);
|
||||
}
|
||||
|
||||
function startGlobalDialogWatcher() {
|
||||
if (dialogObserver) return;
|
||||
let lastDialog = null;
|
||||
dialogObserver = new MutationObserver(() => {
|
||||
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 });
|
||||
}
|
||||
|
||||
// -------- FAB --------
|
||||
function injectFloatingButton() {
|
||||
if (document.getElementById(FAB_ID)) return;
|
||||
const fab = document.createElement('button');
|
||||
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 s = Math.round(cache.pageData.sessionUsage || 0);
|
||||
const w = Math.round(cache.pageData.weeklyUsage || 0);
|
||||
textEl.textContent = `${s}% / ${w}%`;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
async function onFabClick() {
|
||||
const cache = await loadCache();
|
||||
openOverlayPanel(cache);
|
||||
// Auto-refresh si la caché está rancia
|
||||
const ageMin = cache ? cacheAgeMin(cache.scrapedAt) : Infinity;
|
||||
if (ageMin > CACHE_TTL_MS / 60000) {
|
||||
refreshInBackground('overlay-stale');
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Overlay propio --------
|
||||
function openOverlayPanel(cache) {
|
||||
closeOverlayPanel();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = OVERLAY_ID;
|
||||
overlay.className = 'claude-usage-tracker-overlay';
|
||||
overlay.addEventListener('click', e => {
|
||||
if (e.target === overlay) closeOverlayPanel();
|
||||
});
|
||||
|
||||
// Timeout fallback
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
injectTracker();
|
||||
}, 3000);
|
||||
}
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'claude-usage-tracker-overlay-panel';
|
||||
|
||||
// Initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', waitForContent);
|
||||
} else {
|
||||
waitForContent();
|
||||
}
|
||||
const header = document.createElement('div');
|
||||
header.className = 'claude-usage-tracker-overlay-header';
|
||||
const ageMin = cache ? cacheAgeMin(cache.scrapedAt) : Infinity;
|
||||
header.innerHTML = `
|
||||
<span class="claude-usage-tracker-overlay-age">${formatAge(ageMin)}</span>
|
||||
<div class="claude-usage-tracker-overlay-actions">
|
||||
<button class="claude-usage-tracker-overlay-refresh" title="Refrescar ahora">↻ Refrescar</button>
|
||||
<button class="claude-usage-tracker-overlay-close" title="Cerrar">✕</button>
|
||||
</div>
|
||||
`;
|
||||
header.querySelector('.claude-usage-tracker-overlay-close').addEventListener('click', closeOverlayPanel);
|
||||
header.querySelector('.claude-usage-tracker-overlay-refresh').addEventListener('click', () => refreshInBackground('overlay-manual'));
|
||||
|
||||
// Update session time periodically
|
||||
setInterval(() => {
|
||||
const sessionValue = document.querySelector('.session-info-value');
|
||||
if (sessionValue) {
|
||||
injectTracker();
|
||||
const body = document.createElement('div');
|
||||
body.className = 'claude-usage-tracker-overlay-body';
|
||||
|
||||
if (cache && cache.pageData) {
|
||||
body.appendChild(createWeeklyTracker(cache.pageData, cache.dailyData));
|
||||
} else {
|
||||
body.innerHTML = '<div class="claude-usage-tracker-overlay-empty">Sin datos aún. Refrescando…</div>';
|
||||
setTimeout(() => refreshInBackground('overlay-empty'), 0);
|
||||
}
|
||||
}, 60000); // Update every minute
|
||||
|
||||
panel.appendChild(header);
|
||||
panel.appendChild(body);
|
||||
overlay.appendChild(panel);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
document.addEventListener('keydown', overlayEscapeHandler);
|
||||
}
|
||||
|
||||
function overlayEscapeHandler(e) {
|
||||
if (e.key === 'Escape') closeOverlayPanel();
|
||||
}
|
||||
|
||||
function closeOverlayPanel() {
|
||||
const ov = document.getElementById(OVERLAY_ID);
|
||||
if (ov) ov.remove();
|
||||
document.removeEventListener('keydown', overlayEscapeHandler);
|
||||
}
|
||||
|
||||
function setOverlaySpinner(active) {
|
||||
const ov = document.getElementById(OVERLAY_ID);
|
||||
if (!ov) return;
|
||||
let spinner = ov.querySelector('.claude-usage-tracker-overlay-spinner');
|
||||
if (active && !spinner) {
|
||||
spinner = document.createElement('span');
|
||||
spinner.className = 'claude-usage-tracker-overlay-spinner';
|
||||
spinner.textContent = '⟳ actualizando…';
|
||||
const header = ov.querySelector('.claude-usage-tracker-overlay-header');
|
||||
if (header) header.appendChild(spinner);
|
||||
} else if (!active && spinner) {
|
||||
spinner.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function renderOverlayWithCache(cache) {
|
||||
const ov = document.getElementById(OVERLAY_ID);
|
||||
if (!ov) return;
|
||||
const body = ov.querySelector('.claude-usage-tracker-overlay-body');
|
||||
const age = ov.querySelector('.claude-usage-tracker-overlay-age');
|
||||
if (body) {
|
||||
body.innerHTML = '';
|
||||
if (cache && cache.pageData) {
|
||||
body.appendChild(createWeeklyTracker(cache.pageData, cache.dailyData));
|
||||
} else {
|
||||
body.innerHTML = '<div class="claude-usage-tracker-overlay-empty">No he podido cargar los datos. Abre Uso manualmente.</div>';
|
||||
}
|
||||
}
|
||||
if (age && cache) age.textContent = formatAge(cacheAgeMin(cache.scrapedAt));
|
||||
}
|
||||
|
||||
function showOverlayError(msg) {
|
||||
const ov = document.getElementById(OVERLAY_ID);
|
||||
if (!ov) return;
|
||||
const body = ov.querySelector('.claude-usage-tracker-overlay-body');
|
||||
if (body) body.innerHTML = `<div class="claude-usage-tracker-overlay-empty">${msg}</div>`;
|
||||
}
|
||||
|
||||
// -------- Refresh: abrir modal en background, scrapear, cerrar --------
|
||||
function waitForDialogWithData(timeoutMs) {
|
||||
return new Promise(resolve => {
|
||||
const t0 = Date.now();
|
||||
const check = () => {
|
||||
const d = findUsageDialog();
|
||||
if (d && dialogHasData(d)) return resolve(d);
|
||||
if (Date.now() - t0 > timeoutMs) return resolve(null);
|
||||
setTimeout(check, 80);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
async function tryOpenViaPushState() {
|
||||
history.pushState(null, '', '#settings/usage');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
return waitForDialogWithData(700);
|
||||
}
|
||||
|
||||
async function tryOpenViaHash() {
|
||||
location.hash = '#settings/usage';
|
||||
return waitForDialogWithData(700);
|
||||
}
|
||||
|
||||
async function tryOpenViaClick() {
|
||||
const candidates = [...document.querySelectorAll('a, button')]
|
||||
.filter(el => (el.innerText || '').trim() === 'Uso');
|
||||
if (candidates.length === 0) return null;
|
||||
candidates[0].click();
|
||||
return waitForDialogWithData(1500);
|
||||
}
|
||||
|
||||
function closeUsageModal() {
|
||||
const dialog = findUsageDialog();
|
||||
if (!dialog) return;
|
||||
const closeBtn = [...dialog.querySelectorAll('button')]
|
||||
.find(b => /close|cerrar/i.test(b.getAttribute('aria-label') || ''));
|
||||
if (closeBtn) { closeBtn.click(); return; }
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
async function refreshInBackground(reason) {
|
||||
if (refreshPromise) return refreshPromise;
|
||||
refreshPromise = (async () => {
|
||||
console.log('[Claude Usage Tracker] refresh:', reason);
|
||||
setOverlaySpinner(true);
|
||||
|
||||
const previousHash = location.hash;
|
||||
const wasOpen = isUsageDialogOpen();
|
||||
|
||||
let dialog = wasOpen ? findUsageDialog() : null;
|
||||
if (!dialog) dialog = await tryOpenViaPushState();
|
||||
if (!dialog) dialog = await tryOpenViaHash();
|
||||
if (!dialog) dialog = await tryOpenViaClick();
|
||||
|
||||
if (!dialog) {
|
||||
console.warn('[Claude Usage Tracker] no he podido abrir el modal automáticamente');
|
||||
setOverlaySpinner(false);
|
||||
showOverlayError('No he podido refrescar automáticamente — abre Uso manualmente y vuelve.');
|
||||
refreshPromise = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const pageData = parsePageData();
|
||||
let cache = null;
|
||||
if (pageData.weeklyUsage > 0 || pageData.resetDayIndex >= 0) {
|
||||
initSession();
|
||||
const dailyData = calculateDailyUsage(pageData);
|
||||
saveCache(pageData, dailyData, reason === 'overlay-manual' ? 'manual' : 'auto');
|
||||
cache = { pageData, dailyData, scrapedAt: new Date().toISOString() };
|
||||
updateFabState(cache);
|
||||
}
|
||||
|
||||
// Cerrar modal solo si nosotros lo abrimos
|
||||
if (!wasOpen) {
|
||||
closeUsageModal();
|
||||
if (location.hash !== previousHash) {
|
||||
history.replaceState(null, '', location.pathname + location.search + previousHash);
|
||||
}
|
||||
}
|
||||
|
||||
setOverlaySpinner(false);
|
||||
renderOverlayWithCache(cache);
|
||||
refreshPromise = null;
|
||||
})();
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
// -------- 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();
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"permissions": ["storage"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://claude.ai/settings/usage*"],
|
||||
"matches": ["https://claude.ai/*"],
|
||||
"js": ["content.js"],
|
||||
"css": ["styles.css"],
|
||||
"run_at": "document_idle"
|
||||
|
||||
163
styles.css
163
styles.css
@@ -524,3 +524,166 @@
|
||||
color: var(--text-300, #a3a3a3);
|
||||
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;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Overlay propio — abierto al click del FAB
|
||||
=========================================================================== */
|
||||
.claude-usage-tracker-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 999999;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 60px 16px 40px;
|
||||
overflow-y: auto;
|
||||
animation: claude-tracker-fade-in 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes claude-tracker-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-panel {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: #1a1a1d;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
|
||||
padding: 16px 20px 20px;
|
||||
color: #f5f5f5;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-age {
|
||||
font-size: 12px;
|
||||
color: var(--text-400, #a3a3a3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-refresh,
|
||||
.claude-usage-tracker-overlay-close {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #f5f5f5;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-refresh:hover,
|
||||
.claude-usage-tracker-overlay-close:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-close {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-spinner {
|
||||
font-size: 12px;
|
||||
color: #06b6d4;
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
animation: claude-tracker-spin-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes claude-tracker-spin-pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-body {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.claude-usage-tracker-overlay-empty {
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-300, #a3a3a3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Cuando el tracker se inyecta dentro de nuestro overlay, ya está dentro de un
|
||||
contenedor con padding/borde; quitamos su margen superior para que pegue. */
|
||||
.claude-usage-tracker-overlay-body .claude-usage-tracker-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user