Compare commits
6 Commits
ec68336a5d
...
699a8566d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699a8566d0 | ||
|
|
8b6c9dac59 | ||
|
|
5ba8a7437a | ||
|
|
6e5f589156 | ||
|
|
d2bc177376 | ||
|
|
673bd0bced |
356
content.js
356
content.js
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
80
styles.css
80
styles.css
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user