From ee252a2648108229de74a7509c7b1d88a3518f4d Mon Sep 17 00:00:00 2001 From: JavMB Date: Thu, 14 May 2026 19:02:04 +0200 Subject: [PATCH] Enhance styles across various components --- index.html | 11 +- public/doli-favicon.svg | 12 + src/components/BlockedNavigationToast.js | 18 +- src/components/InvoiceItem.js | 41 +-- src/components/Login.js | 12 +- src/components/Sidebar.js | 23 +- src/pages/ClientesPage.js | 131 +++----- src/pages/CreateInvoicePage.js | 20 +- src/pages/DashboardPage.js | 101 +++--- src/pages/Facturas.js | 47 ++- src/pages/SettingsPage.js | 39 ++- src/pages/pagesRegistry.js | 21 +- src/router.js | 5 +- src/services/icons.js | 99 ++++++ src/style.css | 21 +- src/styles/base.css | 93 ++++++ src/styles/clients.css | 14 + src/styles/create-invoice.css | 14 + src/styles/dashboard.css | 111 ++++++- src/styles/facturas.css | 58 +++- src/styles/login.css | 384 +++++++++++++++-------- src/styles/settings.css | 147 ++++++++- src/styles/sidebar.css | 35 ++- 23 files changed, 1048 insertions(+), 409 deletions(-) create mode 100644 public/doli-favicon.svg create mode 100644 src/services/icons.js diff --git a/index.html b/index.html index 2aa003d..09136a6 100755 --- a/index.html +++ b/index.html @@ -1,14 +1,17 @@ - + - + + - doli-front + + + Doli - + diff --git a/public/doli-favicon.svg b/public/doli-favicon.svg new file mode 100644 index 0000000..ca7cd64 --- /dev/null +++ b/public/doli-favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/BlockedNavigationToast.js b/src/components/BlockedNavigationToast.js index c33ff4c..18668c9 100644 --- a/src/components/BlockedNavigationToast.js +++ b/src/components/BlockedNavigationToast.js @@ -1,10 +1,6 @@ -/** - * Muestra un toast de aviso cuando se bloquea la navegación - * @param {string} message - Mensaje a mostrar - * @param {number} duration - Duración en ms (default: 3000) - */ +import { icons } from '../services/icons.js'; + export function showBlockedNavigationToast(message = 'Guarda los cambios antes de salir', duration = 3000) { - // Eliminar toast existente si hay uno const existingToast = document.querySelector('.blocked-nav-toast'); if (existingToast) { existingToast.remove(); @@ -15,23 +11,17 @@ export function showBlockedNavigationToast(message = 'Guarda los cambios antes d toast.innerHTML = /*html*/`
- - - - - + ${icons.info} ${message}
`; document.body.appendChild(toast); - // Animar entrada requestAnimationFrame(() => { toast.classList.add('visible'); }); - // Auto-cerrar después del tiempo especificado setTimeout(() => { toast.classList.remove('visible'); toast.classList.add('hiding'); @@ -42,4 +32,4 @@ export function showBlockedNavigationToast(message = 'Guarda los cambios antes d }, duration); return toast; -} +} \ No newline at end of file diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index 4f480da..d481d8a 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -1,14 +1,14 @@ +import { icons } from '../services/icons.js'; + export function InvoiceItem(invoice, onView) { const item = document.createElement('tr'); item.className = 'invoice-item'; - // Formatear fecha const formatDate = (dateString) => { const date = new Date(dateString); return date.toLocaleDateString('es-ES'); }; - // Formatear moneda const formatCurrency = (amount) => { return new Intl.NumberFormat('es-ES', { style: 'currency', @@ -16,19 +16,17 @@ export function InvoiceItem(invoice, onView) { }).format(amount); }; - // Obtener clase de estado const getStatusClass = (status) => { const statusMap = { - 'draft': 'status-draft', - 'validated': 'status-unpaid', - 'paid': 'status-paid', - 'unpaid': 'status-unpaid', - 'canceled': 'status-canceled' + 'draft': 'badge-draft', + 'validated': 'badge-unpaid', + 'paid': 'badge-paid', + 'unpaid': 'badge-unpaid', + 'canceled': 'badge-draft' }; - return statusMap[status] || 'status-default'; + return statusMap[status] || 'badge-draft'; }; - // Obtener texto de estado const getStatusText = (status) => { const statusTextMap = { 'draft': 'Borrador', @@ -40,35 +38,40 @@ export function InvoiceItem(invoice, onView) { return statusTextMap[status] || status; }; + function escapeHtml(str) { + if (str == null) return ''; + const div = document.createElement('div'); + div.textContent = String(str); + return div.innerHTML; + } + item.innerHTML = /*html*/` - + - - ${getStatusText(invoice.status)} + + ${escapeHtml(getStatusText(invoice.status))} - - ${invoice.clientName || 'Sin nombre'} + ${icons.user} + ${escapeHtml(invoice.clientName || 'Sin nombre')} ${formatDate(invoice.date)} ${formatCurrency(invoice.total)} ${formatCurrency(invoice.remainToPay)} `; - // Invoice number — also opens modal item.querySelector('.btn-invoice-num').addEventListener('click', () => onView?.(invoice.id)); - // Event listener para el botón de ver const viewBtn = item.querySelector('.btn-view'); viewBtn.addEventListener('click', () => onView?.(invoice.id)); return item; -} +} \ No newline at end of file diff --git a/src/components/Login.js b/src/components/Login.js index a8b38b2..d05e12a 100755 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -1,3 +1,5 @@ +import { doliLogo } from '../services/icons.js'; + export function renderLogin(onLogin) { const loginContainer = document.createElement('div'); loginContainer.className = 'login-container'; @@ -6,14 +8,8 @@ export function renderLogin(onLogin) { `; @@ -88,4 +79,4 @@ export function createSidebar(pages, currentPage) { }); return sidebar; -} +} \ No newline at end of file diff --git a/src/pages/ClientesPage.js b/src/pages/ClientesPage.js index cac9eab..4abd6f0 100644 --- a/src/pages/ClientesPage.js +++ b/src/pages/ClientesPage.js @@ -1,11 +1,11 @@ import { ClientItem } from '../components/ClientItem.js'; import { getClients } from '../services/clients.js'; +import { icons } from '../services/icons.js'; export function renderClientesPage() { const container = document.createElement('div'); - container.className = 'clientes-page'; + container.className = 'clientes-page page-enter'; - // Estado de paginación let currentPage = 1; let totalPages = 1; const pageSize = 20; @@ -19,7 +19,10 @@ export function renderClientesPage() {
- +
+ ${icons.search} + +
@@ -36,9 +39,7 @@ export function renderClientesPage() { - - Cargando clientes... - + Cargando clientes...
@@ -52,7 +53,23 @@ export function renderClientesPage() { `; - // Función para aplicar filtros + function displayValue(value) { + if (value === null || value === undefined || value === '' || value === 'null') return 'N/A'; + return value; + } + + function escapeHtml(str) { + if (str == null) return ''; + const div = document.createElement('div'); + div.textContent = String(str); + return div.innerHTML; + } + + function getClientStatusText(status) { + const statusMap = { '0': 'Inactivo', '1': 'Activo' }; + return statusMap[status] || 'N/A'; + } + function applyFilters() { if (!searchTerm) { filteredClients = [...allClients]; @@ -74,40 +91,31 @@ export function renderClientesPage() { renderPage(1); } - // Función para renderizar la página actual function renderPage(page = 1) { const clientsList = container.querySelector('.clients-list'); currentPage = page; totalPages = Math.ceil(filteredClients.length / pageSize); - // Calcular índices para la página const startIndex = (page - 1) * pageSize; const endIndex = Math.min(startIndex + pageSize, filteredClients.length); const clients = filteredClients.slice(startIndex, endIndex); - console.log(`Página ${currentPage}/${totalPages}, Clientes en esta página: ${clients.length}, Total filtrado: ${filteredClients.length}`); - - // Actualizar UI de paginación updatePaginationUI(); - // Limpiar lista clientsList.innerHTML = ''; - // Si no hay clientes if (clients.length === 0) { - clientsList.innerHTML = 'No hay clientes disponibles'; + clientsList.innerHTML = `
${icons.emptyClients}

No hay clientes

Los clientes aparecerán aquí una vez añadidos.

`; return; } - // Renderizar cada cliente clients.forEach(client => { const clientItem = ClientItem(client, handleViewClient); clientsList.appendChild(clientItem); }); } - // Manejar ver cliente function handleViewClient(clientId) { const client = allClients.find(c => c.id === clientId); if (client) { @@ -115,44 +123,22 @@ export function renderClientesPage() { } } - // Función helper para mostrar N/A si el valor es nulo/vacío/inválido - function displayValue(value) { - if (value === null || value === undefined || value === '' || value === 'null') return 'N/A'; - return value; - } - - // Obtener texto de estado del cliente - function getClientStatusText(status) { - const statusMap = { - '0': 'Inactivo', - '1': 'Activo' - }; - return statusMap[status] || 'N/A'; - } - - // Modal simple para ver detalles del cliente function showClientModal(client) { const modal = document.createElement('div'); modal.className = 'client-modal-overlay'; - const iconMail = ``; - const iconPhone = ``; - const iconMobile = ``; - const contactsHtml = client.contacts && client.contacts.length > 0 ? client.contacts.map(contact => `
-
- -
- ${displayValue(contact.firstname)} ${displayValue(contact.lastname)} +
${icons.user}
+ ${escapeHtml(displayValue(contact.firstname))} ${escapeHtml(displayValue(contact.lastname))}
- ${contact.email ? `
${iconMail}${displayValue(contact.email)}
` : ''} - ${contact.phoneMobile ? `
${iconMobile}${displayValue(contact.phoneMobile)}
` : ''} - ${contact.phonePro ? `
${iconPhone}${displayValue(contact.phonePro)}
` : ''} - ${contact.phonePerso ? `
${iconPhone}${displayValue(contact.phonePerso)}
` : ''} + ${contact.email ? `
${icons.mail}${escapeHtml(displayValue(contact.email))}
` : ''} + ${contact.phoneMobile ? `
${icons.mobile}${escapeHtml(displayValue(contact.phoneMobile))}
` : ''} + ${contact.phonePro ? `
${icons.phone}${escapeHtml(displayValue(contact.phonePro))}
` : ''} + ${contact.phonePerso ? `
${icons.phone}${escapeHtml(displayValue(contact.phonePerso))}
` : ''}
`).join('') @@ -161,8 +147,8 @@ export function renderClientesPage() { modal.innerHTML = /*html*/`
-

${displayValue(client.name)}

- +

${escapeHtml(displayValue(client.name))}

+
@@ -170,27 +156,27 @@ export function renderClientesPage() {
ID - ${client.id} + ${escapeHtml(client.id)}
Código - ${displayValue(client.codeClient)} + ${escapeHtml(displayValue(client.codeClient))}
Tipo - ${displayValue(client.typentCode)} + ${escapeHtml(displayValue(client.typentCode))}
Estado - ${getClientStatusText(client.status)} + ${escapeHtml(getClientStatusText(client.status))}
Email - ${displayValue(client.email)} + ${escapeHtml(displayValue(client.email))}
Teléfono - ${displayValue(client.phone)} + ${escapeHtml(displayValue(client.phone))}
@@ -204,7 +190,6 @@ export function renderClientesPage() {
`; - // Cerrar modal const closeBtn = modal.querySelector('.btn-close-modal'); closeBtn.addEventListener('click', () => modal.remove()); modal.addEventListener('click', (e) => { @@ -214,7 +199,6 @@ export function renderClientesPage() { document.body.appendChild(modal); } - // Función para cargar todos los clientes async function loadAllClients() { const clientsList = container.querySelector('.clients-list'); @@ -223,22 +207,16 @@ export function renderClientesPage() { const data = await getClients(1000, 1); - // Manejo de respuesta allClients = Array.isArray(data) ? data : data.data || data.clients || []; filteredClients = [...allClients]; - console.log(`Total de clientes cargados: ${allClients.length}`); - - // Renderizar la primera página renderPage(1); - } catch (error) { console.error('Error al cargar clientes:', error); - clientsList.innerHTML = 'Error al cargar los clientes. Por favor, intenta de nuevo.'; + clientsList.innerHTML = `
${icons.error}

Error al cargar

No se pudieron cargar los clientes. Inténtalo de nuevo.

`; } } - // Función para actualizar la UI de paginación function updatePaginationUI() { const paginationDiv = container.querySelector('.pagination'); const currentPageSpan = container.querySelector('.current-page'); @@ -249,53 +227,38 @@ export function renderClientesPage() { currentPageSpan.textContent = currentPage; totalPagesSpan.textContent = totalPages; - // Mostrar u ocultar la paginación según el número de páginas if (totalPages <= 1) { paginationDiv.style.display = 'none'; } else { paginationDiv.style.display = 'flex'; } - // Deshabilitar botones según corresponda btnPrev.disabled = currentPage === 1; btnNext.disabled = currentPage === totalPages; } - // Event listeners const searchInput = container.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { - clearTimeout(searchInput.debounceTimer); - searchInput.debounceTimer = setTimeout(() => { + clearTimeout(searchInput._debounceTimer); + searchInput._debounceTimer = setTimeout(() => { searchTerm = e.target.value.trim(); applyFilters(); }, 300); }); - // Event listeners de paginación - const btnPrev = container.querySelector('.btn-prev'); - const btnNext = container.querySelector('.btn-next'); - - btnPrev.addEventListener('click', () => { - if (currentPage > 1) { - renderPage(currentPage - 1); - scrollToTop(); - } + container.querySelector('.btn-prev').addEventListener('click', () => { + if (currentPage > 1) { renderPage(currentPage - 1); scrollToTop(); } }); - btnNext.addEventListener('click', () => { - if (currentPage < totalPages) { - renderPage(currentPage + 1); - scrollToTop(); - } + container.querySelector('.btn-next').addEventListener('click', () => { + if (currentPage < totalPages) { renderPage(currentPage + 1); scrollToTop(); } }); - // Función para scroll al inicio function scrollToTop() { container.scrollIntoView({ behavior: 'smooth', block: 'start' }); } - // Cargar todos los clientes al montar el componente loadAllClients(); return container; -} +} \ No newline at end of file diff --git a/src/pages/CreateInvoicePage.js b/src/pages/CreateInvoicePage.js index 433116d..f8ee1c8 100644 --- a/src/pages/CreateInvoicePage.js +++ b/src/pages/CreateInvoicePage.js @@ -1,10 +1,12 @@ import { FormChangeTracker } from '../components/ConfirmExitModal.js'; import { navigationGuards } from '../router.js'; import { apiGet, apiPost } from '../services/apiClient.js'; +import { icons } from '../services/icons.js'; +import { showToast } from '../services/toast.js'; export function renderCreateInvoicePage() { const container = document.createElement('div'); - container.className = 'create-invoice-page'; + container.className = 'create-invoice-page page-enter'; // Tracker de cambios const changeTracker = new FormChangeTracker(); @@ -206,13 +208,13 @@ export function renderCreateInvoicePage() { const clientIdValue = parseInt(container.querySelector('#clientId').value); if (!clientIdValue || isNaN(clientIdValue) || clientIdValue < 1) { - alert('Selecciona un cliente'); + showToast('Selecciona un cliente', 'warning'); return; } const lines = container.querySelectorAll('.line-row-compact'); if (lines.length === 0) { - alert('Añade al menos una línea'); + showToast('Añade al menos una línea', 'warning'); return; } @@ -235,19 +237,19 @@ export function renderCreateInvoicePage() { } if (!isValidDate(dateValue)) { - alert('La fecha de factura no es válida. Introduce una fecha entre los años 2000 y 2100.'); + showToast('La fecha de factura no es válida.', 'warning'); return; } if (!isValidDate(expireDateValue)) { - alert('La fecha de vencimiento no es válida. Introduce una fecha entre los años 2000 y 2100.'); + showToast('La fecha de vencimiento no es válida.', 'warning'); return; } const dateObj = new Date(dateValue); const expireDateObj = new Date(expireDateValue); if (expireDateObj < dateObj) { - alert('La fecha de vencimiento no puede ser anterior a la fecha de factura.'); + showToast('La fecha de vencimiento no puede ser anterior a la de factura.', 'warning'); return; } @@ -281,7 +283,7 @@ export function renderCreateInvoicePage() { }); if (hasEmptyLine) { - alert('Completa todos los campos de las líneas'); + showToast('Completa todos los campos de las líneas', 'warning'); return; } @@ -298,12 +300,12 @@ export function renderCreateInvoicePage() { navigationGuards.unregister(); window.removeEventListener('beforeunload', handleBeforeUnload); - alert(`Factura creada (ID: ${invoiceId})`); + showToast(`Factura creada (ID: ${invoiceId})`, 'success'); window.location.hash = '#invoices'; } catch (error) { console.error('Error:', error); - alert(error.message || 'Error al crear la factura'); + showToast(error.message || 'Error al crear la factura', 'error'); const submitBtn = container.querySelector('.btn-submit-compact'); if (submitBtn) { diff --git a/src/pages/DashboardPage.js b/src/pages/DashboardPage.js index 1b4f49e..9d2b0b3 100644 --- a/src/pages/DashboardPage.js +++ b/src/pages/DashboardPage.js @@ -1,6 +1,7 @@ import { auth } from '../services/auth.js'; import { InvoiceModal } from '../components/InvoiceModal.js'; import { apiGet } from '../services/apiClient.js'; +import { icons } from '../services/icons.js'; import { Chart, BarController, @@ -13,6 +14,13 @@ import { Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend); +function getGreeting() { + const hour = new Date().getHours(); + if (hour < 12) return 'Buenos días'; + if (hour < 20) return 'Buenas tardes'; + return 'Buenas noches'; +} + async function fetchAllInvoices() { const data = await apiGet('/api/Invoices?limit=500&page=1'); return Array.isArray(data) ? data : data.data || data.invoices || []; @@ -75,6 +83,13 @@ function statusDotClass(status) { return map[status] || 'status-draft'; } +function escapeHtml(str) { + if (str == null) return ''; + const div = document.createElement('div'); + div.textContent = String(str); + return div.innerHTML; +} + function openInvoiceModal(invoiceId) { const modal = InvoiceModal(invoiceId, null, () => { }); document.body.appendChild(modal); @@ -91,24 +106,20 @@ export function renderDashboard() { container.innerHTML = /*html*/`
-

Buenos dias, ${userName}

-

Resumen de facturacion · ${year}

+

${getGreeting()}, ${escapeHtml(userName)}

+

Resumen de facturación · ${year}

- +
Facturas emitidas -
- - - - - - -
+
${icons.fileText}
@@ -117,12 +128,7 @@ export function renderDashboard() {
Total facturado -
- - - - -
+
${icons.dollarSign}
@@ -131,12 +137,7 @@ export function renderDashboard() {
Tasa de cobro -
- - - - -
+
${icons.checkCircle}
@@ -145,12 +146,7 @@ export function renderDashboard() {
Pendiente de cobro -
- - - - -
+
${icons.clock}
@@ -160,7 +156,7 @@ export function renderDashboard() {
- Facturacion trimestral + Facturación trimestral ${year}
@@ -186,14 +182,14 @@ export function renderDashboard() {
- Ultimas facturas + Últimas facturas Ver todas
- + @@ -229,7 +225,6 @@ export function renderDashboard() { const { total, totalBilled, paid, unpaid, draft, paidRate, pendingAmount } = calcKPIs(invoices); - // KPIs container.querySelector('#kpi-total').textContent = total.toLocaleString('es-ES'); container.querySelector('#kpi-total-sub').innerHTML = `${paid} pagadas · ` + @@ -248,10 +243,13 @@ export function renderDashboard() { ? `${unpaid} factura${unpaid !== 1 ? 's' : ''} sin cobrar` : 'Sin importes pendientes'; - // Chart.js — quarterly bar chart const canvas = container.querySelector('#db-quarterly-chart'); const q = calcQuarterly(invoices); + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const chartGridColor = isDark ? 'rgba(148, 163, 184, 0.08)' : '#f3f4f6'; + const chartTickColor = isDark ? '#94a3b8' : '#9ca3af'; + new Chart(canvas, { type: 'bar', data: { @@ -261,7 +259,7 @@ export function renderDashboard() { data: q, backgroundColor: 'rgba(37, 99, 235, 0.85)', hoverBackgroundColor: 'rgba(29, 78, 216, 1)', - borderRadius: 4, + borderRadius: 6, borderSkipped: false, barPercentage: 0.5, }] @@ -273,9 +271,9 @@ export function renderDashboard() { legend: { display: false }, tooltip: { callbacks: { label: ctx => ` ${formatCurrency(ctx.parsed.y)}` }, - backgroundColor: '#111827', + backgroundColor: isDark ? '#1e293b' : '#111827', titleColor: '#f9fafb', - bodyColor: '#d1d5db', + bodyColor: isDark ? '#cbd5e1' : '#d1d5db', padding: 10, cornerRadius: 6, displayColors: false, @@ -286,52 +284,48 @@ export function renderDashboard() { grid: { display: false }, border: { display: false }, ticks: { - color: '#6b7280', + color: chartTickColor, font: { size: 12, family: 'Inter, system-ui, sans-serif' } } }, y: { - grid: { color: '#f3f4f6' }, + grid: { color: chartGridColor }, border: { display: false }, ticks: { - color: '#9ca3af', + color: chartTickColor, font: { size: 11, family: 'Inter, system-ui, sans-serif' }, callback: v => v >= 1000 ? `${(v / 1000).toFixed(0)}k €` : `${v} €`, maxTicksLimit: 5, } } }, - animation: { duration: 500, easing: 'easeOutQuart' } + animation: { duration: 600, easing: 'easeOutQuart' } } }); - // Recent activity list const recent = [...invoices] .sort((a, b) => new Date(b.date) - new Date(a.date)) .slice(0, 8); const recentList = container.querySelector('#db-recent-list'); if (recent.length === 0) { - recentList.innerHTML = '

No hay facturas recientes.

'; + recentList.innerHTML = `
${icons.emptyInboxes || ''}No hay facturas recientes.
`; } else { recentList.innerHTML = recent.map(inv => `
- - - - + ${icons.fileText}
- ${inv.number || `#${inv.id}`} - ${inv.clientName || '—'} + ${escapeHtml(inv.number || `#${inv.id}`)} + ${escapeHtml(inv.clientName || '—')}
${formatCurrency(inv.total)} ${formatDate(inv.date)}
- + ${icons.chevron}
`).join(''); @@ -343,7 +337,6 @@ export function renderDashboard() { }); } - // Monitoring table – last 15 const tableRows = [...invoices] .sort((a, b) => new Date(b.date) - new Date(a.date)) .slice(0, 15); @@ -354,8 +347,8 @@ export function renderDashboard() { } else { tbody.innerHTML = tableRows.map(inv => ` - - + + @@ -373,4 +366,4 @@ export function renderDashboard() { })(); return container; -} +} \ No newline at end of file diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index 7db6462..f1fb928 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -1,12 +1,12 @@ import { InvoiceItem } from '../components/InvoiceItem.js'; import { InvoiceModal } from '../components/InvoiceModal.js'; import { apiGet } from '../services/apiClient.js'; +import { icons } from '../services/icons.js'; export function renderFacturasPage() { const container = document.createElement('div'); - container.className = 'facturas-page'; + container.className = 'facturas-page page-enter'; - // Estado de paginación let currentPage = 1; let totalPages = 1; const pageSize = 20; @@ -14,8 +14,6 @@ export function renderFacturasPage() { let filteredInvoices = []; let currentFilter = ''; let searchTerm = ''; - - // Estado de ordenación let sortKey = null; let sortDir = 'asc'; @@ -25,14 +23,17 @@ export function renderFacturasPage() {
- +
+ ${icons.search} + +
- +
@@ -61,9 +62,7 @@ export function renderFacturasPage() {
- - - +
NumeroNúmero Cliente Estado Fecha
${inv.number || `#${inv.id}`}${inv.clientName || '—'}${escapeHtml(inv.number || `#${inv.id}`)}${escapeHtml(inv.clientName || '—')} ${getStatusBadge(inv.status)} ${formatDate(inv.date)} ${formatDate(inv.expireDate)}
Cargando facturas...
Cargando facturas...
@@ -100,7 +99,6 @@ export function renderFacturasPage() { const formatCurrency = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n || 0); - // ── Sort helpers ──────────────────────────────────────────── function sortInvoices(invoices) { if (!sortKey) return invoices; return [...invoices].sort((a, b) => { @@ -135,13 +133,12 @@ export function renderFacturasPage() { }); } - // ── Summary ───────────────────────────────────────────────── function updateSummary(invoices) { const summaryEl = container.querySelector('.invoices-summary'); if (!invoices.length) { summaryEl.style.display = 'none'; return; } - const total = invoices.reduce((s, i) => s + (parseFloat(i.total) || 0), 0); - const pending = invoices.reduce((s, i) => s + (parseFloat(i.remainToPay) || 0), 0); + const total = invoices.reduce((s, i) => s + (parseFloat(i.total) || 0), 0); + const pending = invoices.reduce((s, i) => s + (parseFloat(i.remainToPay) || 0), 0); const paid = total - pending; container.querySelector('[data-summary="count"]').textContent = invoices.length; @@ -151,16 +148,15 @@ export function renderFacturasPage() { summaryEl.style.display = 'flex'; } - // ── Render current page ────────────────────────────────────── function renderPage(page = 1) { const invoicesList = container.querySelector('.invoices-list'); currentPage = page; const sorted = sortInvoices(filteredInvoices); - totalPages = Math.ceil(sorted.length / pageSize); + totalPages = Math.ceil(sorted.length / pageSize); const startIndex = (page - 1) * pageSize; - const invoices = sorted.slice(startIndex, startIndex + pageSize); + const invoices = sorted.slice(startIndex, startIndex + pageSize); updatePaginationUI(); updateSortIcons(); @@ -169,7 +165,7 @@ export function renderFacturasPage() { invoicesList.innerHTML = ''; if (invoices.length === 0) { - invoicesList.innerHTML = 'No hay facturas disponibles'; + invoicesList.innerHTML = `
${icons.emptyInvoices}

No hay facturas

Crea tu primera factura para comenzar.

`; return; } @@ -179,7 +175,6 @@ export function renderFacturasPage() { }); } - // Manejar ver/editar factura function handleViewInvoice(invoiceId) { const modal = InvoiceModal(invoiceId, null, () => { loadAllInvoices(); @@ -187,7 +182,6 @@ export function renderFacturasPage() { document.body.appendChild(modal); } - // Función para cargar todas las facturas async function loadAllInvoices() { const invoicesList = container.querySelector('.invoices-list'); @@ -206,7 +200,7 @@ export function renderFacturasPage() { renderPage(1); } catch (error) { console.error('Error al cargar facturas:', error); - invoicesList.innerHTML = 'Error al cargar las facturas. Por favor, intenta de nuevo.'; + invoicesList.innerHTML = `
${icons.error}

Error al cargar

No se pudieron cargar las facturas. Inténtalo de nuevo.

`; } } @@ -214,23 +208,21 @@ export function renderFacturasPage() { loadAllInvoices(); } - // Función para actualizar la UI de paginación function updatePaginationUI() { - const paginationDiv = container.querySelector('.pagination'); + const paginationDiv = container.querySelector('.pagination'); const currentPageSpan = container.querySelector('.current-page'); const totalPagesSpan = container.querySelector('.total-pages'); const btnPrev = container.querySelector('.btn-prev'); const btnNext = container.querySelector('.btn-next'); currentPageSpan.textContent = currentPage; - totalPagesSpan.textContent = totalPages; + totalPagesSpan.textContent = totalPages; paginationDiv.style.display = totalPages <= 1 ? 'none' : 'flex'; btnPrev.disabled = currentPage === 1; btnNext.disabled = currentPage === totalPages; } - // ── Sort click listeners ───────────────────────────────────── container.querySelectorAll('th.sortable').forEach(th => { th.style.cursor = 'pointer'; th.addEventListener('click', () => { @@ -245,7 +237,6 @@ export function renderFacturasPage() { }); }); - // ── Other event listeners ──────────────────────────────────── container.querySelector('.btn-new-invoice').addEventListener('click', () => { window.location.hash = '#create-invoice'; }); @@ -253,8 +244,8 @@ export function renderFacturasPage() { const searchInput = container.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { const inputValue = e.target.value.trim(); - clearTimeout(searchInput.debounceTimer); - searchInput.debounceTimer = setTimeout(() => { + clearTimeout(searchInput._debounceTimer); + searchInput._debounceTimer = setTimeout(() => { searchTerm = /\d/.test(inputValue) ? inputValue : ''; applyFilters(); }, 500); @@ -280,4 +271,4 @@ export function renderFacturasPage() { loadAllInvoices(); return container; -} +} \ No newline at end of file diff --git a/src/pages/SettingsPage.js b/src/pages/SettingsPage.js index df5a54b..ca0c7a6 100644 --- a/src/pages/SettingsPage.js +++ b/src/pages/SettingsPage.js @@ -1,4 +1,5 @@ import { isDarkTheme, toggleTheme } from '../services/theme.js'; +import { icons } from '../services/icons.js'; function decodeTokenPayload(token) { try { @@ -50,10 +51,12 @@ export function renderSettingsPage() { const container = document.createElement('div'); container.className = 'settings-page'; + const dark = isDarkTheme(); + container.innerHTML = `
-

Configuracion

-

Preferencias visuales y estado de sesion.

+

Configuración

+

Preferencias visuales y estado de sesión.

@@ -64,19 +67,33 @@ export function renderSettingsPage() {

Modo oscuro

Cambia entre tema claro y oscuro.

- +
-
-

Sesion

+
+

Sesión

- Caducidad del token +
+ ${icons.info} + Caducidad del token +
-
- Tiempo restante +
+ ${icons.clock} + Tiempo restante +
-
@@ -89,9 +106,9 @@ export function renderSettingsPage() { const remainingEl = container.querySelector('#token-remaining'); const renderThemeButton = () => { - const dark = isDarkTheme(); - themeBtn.textContent = dark ? 'Activado' : 'Desactivado'; - themeBtn.setAttribute('aria-pressed', dark ? 'true' : 'false'); + const isDark = isDarkTheme(); + themeBtn.classList.toggle('settings-theme-toggle--active', isDark); + themeBtn.setAttribute('aria-pressed', isDark ? 'true' : 'false'); }; const renderTokenInfo = () => { @@ -118,4 +135,4 @@ export function renderSettingsPage() { container.cleanup = () => clearInterval(intervalId); return container; -} +} \ No newline at end of file diff --git a/src/pages/pagesRegistry.js b/src/pages/pagesRegistry.js index e1ef6c2..2884d55 100755 --- a/src/pages/pagesRegistry.js +++ b/src/pages/pagesRegistry.js @@ -3,13 +3,13 @@ import { renderFacturasPage } from './Facturas.js'; import { renderCreateInvoicePage } from './CreateInvoicePage.js'; import { renderClientesPage } from './ClientesPage.js'; import { renderSettingsPage } from './SettingsPage.js'; +import { icons } from '../services/icons.js'; -// Registry of all pages available in the application export const pagesRegistry = [ { route: 'dashboard', name: 'Dashboard', - icon: '', + icon: icons.dashboard, voicePatterns: ['dashboard', 'inicio', 'ir al inicio', 'ir al dashboard', 'resumen', 'home', 'panel', 'principal'], requiresAuth: true, showInSidebar: true, @@ -18,7 +18,7 @@ export const pagesRegistry = [ { route: 'invoices', name: 'Facturas', - icon: '', + icon: icons.invoices, voicePatterns: ['facturas', 'ver facturas', 'ir a facturas', 'lista facturas', 'mis facturas', 'listado facturas'], requiresAuth: true, showInSidebar: true, @@ -27,16 +27,16 @@ export const pagesRegistry = [ { route: 'create-invoice', name: 'Nueva Factura', - icon: '+', + icon: icons.plus, voicePatterns: ['nueva factura', 'crear factura', 'factura nueva', 'añadir factura'], requiresAuth: true, - showInSidebar: false, // No mostrar en sidebar + showInSidebar: false, render: renderCreateInvoicePage }, { route: 'clients', name: 'Clientes', - icon: '', + icon: icons.clients, voicePatterns: ['clientes', 'ver clientes', 'ir a clientes', 'lista clientes', 'mis clientes'], requiresAuth: true, showInSidebar: true, @@ -45,25 +45,24 @@ export const pagesRegistry = [ { route: 'settings', name: 'Configuración', - icon: '', voicePatterns: ['configuración', 'configuracion', 'ajustes', 'settings', 'preferencias'], requiresAuth: true, + icon: icons.settings, + voicePatterns: ['configuración', 'configuracion', 'ajustes', 'settings', 'preferencias'], + requiresAuth: true, showInSidebar: true, render: renderSettingsPage } ]; -// Get pages that the user can access export function getAvailablePages(isAuthenticated) { if (!isAuthenticated) { return []; } - // Filter pages based on authentication requirement and sidebar visibility return pagesRegistry.filter(page => { if (page.requiresAuth && !isAuthenticated) return false; return page.showInSidebar; }); } -// Get page by route export function getPageByRoute(route) { return pagesRegistry.find(page => page.route === route); -} +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index 61428c6..bc6ef05 100755 --- a/src/router.js +++ b/src/router.js @@ -118,8 +118,9 @@ export function initRouter() { return; } - // Render page content using registry - const pageContent = page.render(); + // Render page content using registry (supports async render) + const maybePromise = page.render(); + const pageContent = maybePromise instanceof Promise ? await maybePromise : maybePromise; // Mobile sidebar: backdrop + topbar const sidebarBackdrop = document.createElement('div'); diff --git a/src/services/icons.js b/src/services/icons.js new file mode 100644 index 0000000..adcde84 --- /dev/null +++ b/src/services/icons.js @@ -0,0 +1,99 @@ +export const doliLogo = (size = 28) => ` + + + + + + + + + + + +`; + +export const doliLogoFull = (height = 24) => ` + + + + + + + + + + + + Doli +`; + +export const icons = { + dashboard: ``, + + invoices: ``, + + clients: ``, + + settings: ``, + + logout: ``, + + menu: ``, + + chevron: ``, + + search: ``, + + plus: ``, + + close: ``, + + dollarSign: ``, + + checkCircle: ``, + + clock: ``, + + fileText: ``, + + mail: ``, + + phone: ``, + + mobile: ``, + + user: ``, + + calendar: ``, + + eye: ``, + + lock: ``, + + sort: ``, + + emptyInvoices: ` + + + + + + + +`, + + emptyClients: ` + + + + +`, + + error: ``, + + sun: ``, + + moon: ``, + + info: `` +}; \ No newline at end of file diff --git a/src/style.css b/src/style.css index 4e1a9e0..a389606 100755 --- a/src/style.css +++ b/src/style.css @@ -41,19 +41,23 @@ --red-600: #dc2626; --red-700: #b91c1c; - /* Semantic tokens */ --primary: var(--blue-600); --primary-hover: var(--blue-700); + --primary-light: rgba(37, 99, 235, 0.06); + --primary-subtle: rgba(37, 99, 235, 0.08); --success: var(--green-600); --success-hover: var(--green-700); --warning: var(--amber-600); + --warning-hover: #a16207; --danger: var(--red-500); --danger-hover: var(--red-600); --card-bg: #ffffff; --border-color: var(--gray-200); + --border-subtle: var(--gray-100); --text-primary: var(--gray-900); --text-secondary: var(--gray-500); + --text-tertiary: var(--gray-400); --bg-page: var(--gray-50); --bg-subtle: var(--gray-100); @@ -92,6 +96,19 @@ -moz-osx-font-smoothing: grayscale; } +/* ── Large / Presentation Displays ── */ +@media (min-width: 1600px) { + :root { + font-size: 15px; + } +} + +@media (min-width: 2200px) { + :root { + font-size: 16px; + } +} + :root[data-theme='dark'] { --gray-50: #0f172a; --gray-100: #111827; @@ -108,8 +125,10 @@ --border-color: #1f2a3d; --text-primary: #f8fafc; --text-secondary: #94a3b8; + --text-tertiary: #64748b; --bg-page: #070d18; --bg-subtle: #0f172a; + --border-subtle: #1f2a3d; color-scheme: dark; } diff --git a/src/styles/base.css b/src/styles/base.css index 7527d02..e503053 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -255,3 +255,96 @@ button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } transition: background 0.15s ease; } .empty-state .btn-empty-action:hover { background: var(--primary-hover); } + +/* ── Page entrance animation ── */ +.page-enter { + animation: pageEnter 0.3s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes pageEnter { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Stagger animation for KPI cards ── */ +.db-kpi-card { animation: fadeInUp 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; } +.db-kpi-card:nth-child(1) { animation-delay: 0ms; } +.db-kpi-card:nth-child(2) { animation-delay: 80ms; } +.db-kpi-card:nth-child(3) { animation-delay: 160ms; } +.db-kpi-card:nth-child(4) { animation-delay: 240ms; } + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Hover lift for cards ── */ +.db-kpi-card, +.settings-card, +.db-chart-card, +.db-recent-card, +.db-table-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.db-kpi-card:hover, +.db-chart-card:hover, +.db-recent-card:hover, +.db-table-card:hover { + transform: translateY(-1px); +} + +/* ── Status badge enhanced ── */ +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + border-radius: 99px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.03em; + line-height: 1.6; +} + +.badge::before { + content: ''; + width: 5px; + height: 5px; + border-radius: 50%; + flex-shrink: 0; +} + +.badge-draft { + background: var(--gray-100); + color: var(--gray-600); +} +.badge-draft::before { background: var(--gray-400); } + +.badge-unpaid { + background: var(--amber-50); + color: var(--amber-600); +} +.badge-unpaid::before { background: var(--amber-600); } + +.badge-paid { + background: var(--green-50); + color: var(--green-600); +} +.badge-paid::before { background: var(--green-600); } + +:root[data-theme='dark'] .badge-draft { + background: rgba(148, 163, 184, 0.14); + color: #cbd5e1; +} +:root[data-theme='dark'] .badge-draft::before { background: #64748b; } +:root[data-theme='dark'] .badge-unpaid { + background: rgba(245, 158, 11, 0.15); + color: #fcd34d; +} +:root[data-theme='dark'] .badge-unpaid::before { background: #fcd34d; } +:root[data-theme='dark'] .badge-paid { + background: rgba(34, 197, 94, 0.18); + color: #86efac; +} +:root[data-theme='dark'] .badge-paid::before { background: #86efac; } diff --git a/src/styles/clients.css b/src/styles/clients.css index 5b2b8d6..ef07b53 100644 --- a/src/styles/clients.css +++ b/src/styles/clients.css @@ -8,6 +8,15 @@ margin: 0 auto; } +@media (min-width: 1600px) { + .clientes-page { max-width: 1600px; } + .clientes-header h1 { font-size: 1.5rem; } +} + +@media (min-width: 2200px) { + .clientes-page { max-width: 2000px; } +} + .clientes-header { margin-bottom: var(--space-5); } @@ -29,6 +38,11 @@ max-width: 400px; } +.clientes-filters .search-wrapper { + flex: 1; + max-width: 400px; +} + .client-avatar-col { width: 48px; } diff --git a/src/styles/create-invoice.css b/src/styles/create-invoice.css index 86a4399..7e19a5b 100644 --- a/src/styles/create-invoice.css +++ b/src/styles/create-invoice.css @@ -9,6 +9,20 @@ min-height: 100vh; } +@media (min-width: 1600px) { + .create-invoice-page { max-width: 1100px; } + .invoice-header-compact h1 { font-size: 1.5rem; } + .form-field-compact input, + .form-field-compact select, + .form-field-compact textarea { height: 42px; font-size: 15px; } + .btn-submit-compact, + .btn-cancel-compact { height: 44px; font-size: 15px; } + .line-desc-compact, + .line-qty-compact, + .line-price-compact, + .line-tax-compact { height: 38px; font-size: 14px; } +} + .invoice-header-compact { display: flex; justify-content: space-between; diff --git a/src/styles/dashboard.css b/src/styles/dashboard.css index ac341e6..c540d06 100644 --- a/src/styles/dashboard.css +++ b/src/styles/dashboard.css @@ -55,8 +55,8 @@ } .db-kpi-icon { - width: 28px; - height: 28px; + width: 32px; + height: 32px; border-radius: var(--radius-md); background: var(--gray-100); display: flex; @@ -66,6 +66,26 @@ flex-shrink: 0; } +.db-kpi-icon--blue { + background: rgba(37, 99, 235, 0.1); + color: var(--blue-600); +} + +.db-kpi-icon--green { + background: rgba(22, 163, 74, 0.1); + color: var(--green-600); +} + +.db-kpi-icon--emerald { + background: rgba(5, 150, 105, 0.1); + color: #059669; +} + +.db-kpi-icon--amber { + background: rgba(217, 119, 6, 0.1); + color: var(--amber-600); +} + .db-kpi-label { font-size: 11px; font-weight: 500; @@ -413,12 +433,69 @@ .db-greeting { font-size: 1.1rem; } } +/* ── Large / Presentation Displays ── */ +@media (min-width: 1600px) { + .db-kpi-row { + grid-template-columns: repeat(4, 1fr); + gap: var(--space-6); + } + .db-kpi-value { font-size: 2rem; } + .db-kpi-label { font-size: 12px; } + .db-greeting { font-size: 1.75rem; } + .db-subtitle { font-size: 15px; } + .db-card-title { font-size: 15px; } + .db-chart-body { height: 340px; } + .db-recent-list { max-height: 400px; } + .dashboard-subtitle { font-size: 15px; } +} + +@media (min-width: 2200px) { + .db-kpi-value { font-size: 2.5rem; } + .db-greeting { font-size: 2rem; } + .db-kpi-card { padding: var(--space-6); } + .db-card-header { padding: var(--space-5) var(--space-6); } + .db-chart-body { height: 420px; } +} + .dashboard-subtitle { color: var(--text-secondary); font-size: 13px; margin: var(--space-1) 0 0 0; } +/* Logout button with icon */ +.logout-button { + display: inline-flex; + align-items: center; + gap: var(--space-2); + background-color: transparent; + color: var(--text-secondary); + padding: var(--space-2) var(--space-3); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + font-size: 13px; + font-weight: 500; + font-family: inherit; + transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); + white-space: nowrap; +} + +.logout-button:hover { + color: var(--danger); + border-color: var(--danger); + background-color: var(--red-50); +} + +.logout-button:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.logout-button svg { + flex-shrink: 0; +} + /* Dark mode UX tune */ :root[data-theme='dark'] .db-kpi-card, :root[data-theme='dark'] .db-chart-card, @@ -458,3 +535,33 @@ background: rgba(148, 163, 184, 0.18); color: #cbd5e1; } + +:root[data-theme='dark'] .db-kpi-icon--blue { + background: rgba(59, 130, 246, 0.15); + color: #93c5fd; +} + +:root[data-theme='dark'] .db-kpi-icon--green { + background: rgba(34, 197, 94, 0.15); + color: #86efac; +} + +:root[data-theme='dark'] .db-kpi-icon--emerald { + background: rgba(16, 185, 129, 0.15); + color: #6ee7b7; +} + +:root[data-theme='dark'] .db-kpi-icon--amber { + background: rgba(245, 158, 11, 0.15); + color: #fcd34d; +} + +:root[data-theme='dark'] .logout-button { + background-color: transparent; + border-color: rgba(148, 163, 184, 0.2); +} + +:root[data-theme='dark'] .logout-button:hover { + background-color: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); +} diff --git a/src/styles/facturas.css b/src/styles/facturas.css index 96d395c..2ae6e36 100644 --- a/src/styles/facturas.css +++ b/src/styles/facturas.css @@ -12,7 +12,7 @@ .facturas-header h1 { font-size: 1.25rem; font-weight: 600; } .btn-new-invoice { - background-color: var(--primary); + background: linear-gradient(135deg, var(--blue-600), var(--blue-700)); color: white; padding: var(--space-2) var(--space-4); border: none; @@ -20,11 +20,25 @@ cursor: pointer; font-weight: 500; font-size: 14px; - transition: background-color var(--transition-fast); + font-family: inherit; + transition: all 0.2s ease; height: 36px; + display: inline-flex; + align-items: center; + gap: var(--space-1); + letter-spacing: 0.01em; } -.btn-new-invoice:hover { background-color: var(--primary-hover); } +.btn-new-invoice:hover { + background: linear-gradient(135deg, var(--blue-700), #1e3a8a); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); +} + +.btn-new-invoice:active { + transform: translateY(0); + box-shadow: none; +} .facturas-filters { display: flex; @@ -56,6 +70,30 @@ .search-input { flex: 1; max-width: 320px; } .filter-status { min-width: 180px; cursor: pointer; } +.search-wrapper { + position: relative; + display: flex; + align-items: center; + flex: 1; + max-width: 340px; +} + +.search-icon { + position: absolute; + left: 10px; + color: var(--text-tertiary, var(--gray-400)); + display: flex; + align-items: center; + pointer-events: none; +} + +.search-input--with-icon { + padding-left: 34px !important; + max-width: 100%; + flex: none; + width: 100%; +} + .invoices-table-container, .clients-table-container { background: var(--card-bg); @@ -403,3 +441,17 @@ display: none; } } + +@media (min-width: 1600px) { + .facturas-page { max-width: 1600px; } + .invoices-table th, + .invoices-table td { font-size: 14px; padding: var(--space-3) var(--space-5); } + .facturas-header h1 { font-size: 1.5rem; } + .filter-status { min-width: 220px; } +} + +@media (min-width: 2200px) { + .facturas-page { max-width: 2000px; } + .invoices-table th, + .invoices-table td { padding: var(--space-4) var(--space-6); } +} diff --git a/src/styles/login.css b/src/styles/login.css index a33b989..c606c06 100644 --- a/src/styles/login.css +++ b/src/styles/login.css @@ -1,221 +1,355 @@ /* ============================================ - Login + Session Notices - ============================================ */ + Login + Session Notices + ============================================ */ .login-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - width: 100%; - background: linear-gradient(145deg, var(--bg-page) 0%, var(--blue-50) 100%); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + width: 100%; + background: linear-gradient(135deg, var(--bg-page) 0%, var(--blue-50) 50%, var(--bg-page) 100%); + position: relative; + overflow: hidden; +} + +.login-container::before { + content: ''; + position: absolute; + width: 500px; + height: 500px; + border-radius: 50%; + background: radial-gradient(circle, rgba(37, 99, 235, 0.08) 0%, transparent 70%); + top: -150px; + right: -100px; + pointer-events: none; + animation: loginPulse 8s ease-in-out infinite; +} + +.login-container::after { + content: ''; + position: absolute; + width: 400px; + height: 400px; + border-radius: 50%; + background: radial-gradient(circle, rgba(37, 99, 235, 0.05) 0%, transparent 70%); + bottom: -120px; + left: -80px; + pointer-events: none; + animation: loginPulse 10s ease-in-out infinite reverse; +} + +@keyframes loginPulse { + 0%, 100% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(1.1); opacity: 1; } } .login-card { - background: var(--card-bg); - padding: var(--space-8); - border-radius: 12px; - box-shadow: var(--shadow-md); - width: 100%; - max-width: 390px; - border: 1px solid var(--border-color); + background: var(--card-bg); + padding: var(--space-8); + border-radius: 16px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 10px 30px -5px rgba(0, 0, 0, 0.08); + width: 100%; + max-width: 400px; + border: 1px solid var(--border-color); + position: relative; + z-index: 1; + animation: loginCardIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes loginCardIn { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } } .login-brand { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - margin-bottom: var(--space-6); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: var(--space-5); } .login-logo-icon { - width: 40px; - height: 40px; - background: var(--primary); - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - color: white; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + animation: logoFloat 3s ease-in-out infinite; + filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.25)); +} + +@keyframes logoFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-3px); } } .login-brand-name { - font-size: 1.25rem; - font-weight: 700; - color: var(--text-primary); - letter-spacing: -0.02em; + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; } .login-title { - color: var(--text-primary); - margin: 0 0 var(--space-1) 0; - font-size: 1.125rem; - text-align: center; - font-weight: 600; + color: var(--text-primary); + margin: 0 0 var(--space-1) 0; + font-size: 1.25rem; + text-align: center; + font-weight: 600; } .login-subtitle { - color: var(--text-secondary); - font-size: 13px; - text-align: center; - margin: 0 0 var(--space-5) 0; + color: var(--text-secondary); + font-size: 13px; + text-align: center; + margin: 0 0 var(--space-5) 0; } .form-group { - margin-bottom: var(--space-4); + margin-bottom: var(--space-4); } .form-group label { - display: block; - color: var(--text-secondary); - margin-bottom: var(--space-1); - font-weight: 500; - font-size: 13px; + display: block; + color: var(--text-secondary); + margin-bottom: var(--space-1); + font-weight: 500; + font-size: 13px; } .form-group input { - width: 100%; - padding: var(--space-2) var(--space-3); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--card-bg); - color: var(--text-primary); - font-size: 14px; - font-family: inherit; - box-sizing: border-box; - transition: border-color var(--transition-fast); - height: 36px; + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--card-bg); + color: var(--text-primary); + font-size: 14px; + font-family: inherit; + box-sizing: border-box; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + height: 40px; } .form-group input:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12); + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.form-group input:focus:not(:focus-visible) { + box-shadow: none; } .login-button { - width: 100%; - padding: var(--space-2) var(--space-4); - background-color: var(--primary); - color: white; - border: none; - border-radius: var(--radius-md); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background-color var(--transition-fast); - height: 36px; - margin-top: var(--space-2); + width: 100%; + padding: var(--space-3) var(--space-4); + background: linear-gradient(135deg, var(--blue-600), var(--blue-700)); + color: white; + border: none; + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + height: 42px; + margin-top: var(--space-2); + letter-spacing: 0.01em; + position: relative; + overflow: hidden; +} + +.login-button::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent); + opacity: 0; + transition: opacity 0.2s ease; } .login-button:hover { - background-color: var(--primary-hover); - border-color: transparent; + background: linear-gradient(135deg, var(--blue-700), #1e3a8a); + border-color: transparent; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); +} + +.login-button:hover::after { + opacity: 1; +} + +.login-button:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2); +} + +.login-button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: none; } .login-error { - color: var(--danger); - text-align: center; - margin-top: var(--space-3); - font-size: 13px; + color: var(--danger); + text-align: center; + margin-top: var(--space-3); + font-size: 13px; + min-height: 20px; } .session-warning-overlay, .connection-lost-overlay { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-4); + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-4); } .session-warning-overlay { - background: rgba(17, 24, 39, 0.45); - z-index: 3000; + background: rgba(17, 24, 39, 0.45); + z-index: 3000; + backdrop-filter: blur(4px); } .connection-lost-overlay { - background: rgba(17, 24, 39, 0.55); - z-index: 4000; + background: rgba(17, 24, 39, 0.55); + z-index: 4000; + backdrop-filter: blur(4px); } .session-warning-modal, .connection-lost-modal { - width: 100%; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - box-shadow: var(--shadow-lg); - padding: var(--space-6); + width: 100%; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: var(--shadow-lg); + padding: var(--space-6); } .session-warning-modal { max-width: 430px; } .connection-lost-modal { max-width: 460px; } .session-warning-modal h3 { - margin: 0 0 var(--space-3) 0; - font-size: 1.15rem; + margin: 0 0 var(--space-3) 0; + font-size: 1.15rem; } .connection-lost-modal h3 { - margin: 0 0 var(--space-2) 0; - color: var(--danger); + margin: 0 0 var(--space-2) 0; + color: var(--danger); } .session-warning-modal p, .connection-lost-modal p { - margin: 0; - color: var(--text-secondary); + margin: 0; + color: var(--text-secondary); } .session-warning-actions { - margin-top: var(--space-5); - display: flex; - justify-content: flex-end; - gap: var(--space-2); + margin-top: var(--space-5); + display: flex; + justify-content: flex-end; + gap: var(--space-2); } .session-warning-logout { - background: transparent; - border: 1px solid var(--border-color); - color: var(--text-secondary); + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); } .session-warning-logout:hover { - background: var(--gray-100); + background: var(--gray-100); } .session-warning-continue { - background: var(--primary); + background: var(--primary); } .connection-lost-sub { - margin-top: var(--space-3) !important; - font-size: 0.92rem; + margin-top: var(--space-3) !important; + font-size: 0.92rem; } .connection-lost-login-btn { - margin-top: var(--space-5); - width: 100%; + margin-top: var(--space-5); + width: 100%; } @media (max-width: 640px) { - .login-card { - margin: var(--space-4); - padding: var(--space-6); - } + .login-card { + margin: var(--space-4); + padding: var(--space-6); + } - .session-warning-actions { - flex-direction: column; - } + .session-warning-actions { + flex-direction: column; + } - .session-warning-actions button { - width: 100%; - } + .session-warning-actions button { + width: 100%; + } +} + +@media (min-width: 1600px) { + .login-card { + max-width: 440px; + padding: var(--space-10); + border-radius: 20px; + } + + .login-title { + font-size: 1.4rem; + } + + .login-subtitle { + font-size: 15px; + } + + .login-logo-icon { + width: 64px; + height: 64px; + } + + .form-group label { + font-size: 14px; + } + + .form-group input { + height: 46px; + font-size: 15px; + padding: var(--space-3) var(--space-4); + } + + .login-button { + height: 48px; + font-size: 16px; + } } :root[data-theme='dark'] .login-container { - background: linear-gradient(145deg, var(--bg-page) 0%, rgba(30, 58, 110, 0.2) 100%); + background: linear-gradient(135deg, var(--bg-page) 0%, rgba(30, 58, 110, 0.15) 50%, var(--bg-page) 100%); } + +:root[data-theme='dark'] .login-container::before { + background: radial-gradient(circle, rgba(59, 130, 246, 0.06) 0%, transparent 70%); +} + +:root[data-theme='dark'] .login-container::after { + background: radial-gradient(circle, rgba(59, 130, 246, 0.04) 0%, transparent 70%); +} + +:root[data-theme='dark'] .login-card { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 10px 30px -5px rgba(0, 0, 0, 0.2); + border-color: var(--border-color); +} + +:root[data-theme='dark'] .login-logo-icon { + filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.2)); +} \ No newline at end of file diff --git a/src/styles/settings.css b/src/styles/settings.css index b52ec45..5395e01 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -8,18 +8,29 @@ padding: var(--space-6); } +@media (min-width: 1600px) { + .settings-page { max-width: 780px; } + .settings-card { padding: var(--space-6); } + .settings-header h1 { font-size: 1.5rem; } +} + .settings-header { margin-bottom: var(--space-6); } +.settings-header h1 { + margin: 0 0 var(--space-1) 0; +} + .settings-header p { - margin-top: var(--space-2); + margin: 0; color: var(--text-secondary); + font-size: 13px; } .settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + display: flex; + flex-direction: column; gap: var(--space-4); } @@ -32,8 +43,9 @@ } .settings-card h2 { - margin-bottom: var(--space-4); + margin: 0 0 var(--space-4) 0; font-size: 1.05rem; + font-weight: 600; } .settings-row { @@ -56,14 +68,90 @@ color: var(--text-secondary); } +/* ── Toggle Switch ── */ .settings-theme-toggle { - min-width: 116px; + position: relative; + background: none; + border: none; + padding: 0; + cursor: pointer; + outline: none; +} + +.settings-theme-toggle:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 4px; + border-radius: 4px; +} + +.toggle-track { + display: flex; + align-items: center; + width: 56px; + height: 30px; + border-radius: 15px; + background: var(--gray-300); + position: relative; + transition: background-color 0.25s ease; + overflow: hidden; +} + +.settings-theme-toggle--active .toggle-track { + background: var(--blue-600); +} + +.toggle-thumb { + position: absolute; + top: 3px; + left: 3px; + width: 24px; + height: 24px; + border-radius: 50%; + background: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 2; +} + +.settings-theme-toggle--active .toggle-thumb { + transform: translateX(26px); +} + +.toggle-icons { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 7px; + position: relative; + z-index: 1; + pointer-events: none; +} + +.toggle-icon-sun, +.toggle-icon-moon { + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.7); + transition: opacity 0.2s ease; +} + +.toggle-icon-sun { opacity: 1; } +.toggle-icon-moon { opacity: 0.6; } + +.settings-theme-toggle--active .toggle-icon-sun { opacity: 0.6; } +.settings-theme-toggle--active .toggle-icon-moon { opacity: 1; } + +/* ── Session Section ── */ +.settings-card--session { + background: var(--card-bg); } .settings-session-list { display: flex; flex-direction: column; - gap: var(--space-4); + gap: var(--space-3); } .settings-session-item { @@ -71,7 +159,7 @@ justify-content: space-between; align-items: center; gap: var(--space-3); - padding-bottom: var(--space-3); + padding: var(--space-3) 0; border-bottom: 1px dashed var(--border-color); } @@ -80,6 +168,47 @@ padding-bottom: 0; } -.settings-token-expired { - color: var(--danger); +.settings-session-item-label { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: 0.9rem; + color: var(--text-primary); + font-weight: 500; } + +.settings-session-icon { + display: flex; + align-items: center; + color: var(--text-tertiary, var(--text-secondary)); +} + +.settings-token-expired { + color: var(--danger) !important; + font-weight: 600; +} + +/* ── Dark mode ── */ +:root[data-theme='dark'] .toggle-track { + background: var(--gray-700); +} + +:root[data-theme='dark'] .settings-theme-toggle--active .toggle-track { + background: var(--blue-600); +} + +:root[data-theme='dark'] .toggle-thumb { + background: #e2e8f0; +} + +@media (max-width: 580px) { + .settings-page { + padding: var(--space-4); + } + + .settings-session-item { + flex-direction: column; + align-items: flex-start; + gap: var(--space-1); + } +} \ No newline at end of file diff --git a/src/styles/sidebar.css b/src/styles/sidebar.css index e561ac8..9559cc3 100644 --- a/src/styles/sidebar.css +++ b/src/styles/sidebar.css @@ -38,15 +38,13 @@ } .sidebar-logo-icon { - width: 26px; - height: 26px; - background: var(--primary); - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - color: white; - flex-shrink: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(37, 99, 235, 0.2)); } .sidebar-header h2 { @@ -270,6 +268,25 @@ padding: var(--space-6); } +@media (min-width: 1600px) { + .sidebar { width: 250px; } + .main-content > div { + max-width: 1440px; + padding: var(--space-8); + } +} + +@media (min-width: 2200px) { + .sidebar { width: 280px; } + .sidebar-link { font-size: 15px; padding: var(--space-3) var(--space-4); } + .sidebar-avatar { width: 32px; height: 32px; font-size: 13px; } + .sidebar-username { font-size: 13px; } + .main-content > div { + max-width: 1800px; + padding: var(--space-10); + } +} + /* ── Mobile topbar (hidden on desktop) ── */ .mobile-topbar { display: none;