From b93f711539aab22e3ca153426f588d7957161c5a Mon Sep 17 00:00:00 2001 From: maheshrijal <62394512+maheshrijal@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:45:50 +0530 Subject: [PATCH] feat: improve accessibility, performance, and SEO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accessibility (Web Interface Guidelines): - Add aria-labels to icon buttons and dynamic elements - Add skip link for keyboard navigation - Add proper focus-visible states (remove outline:none anti-pattern) - Add keyboard support for history items (role=button + keydown) - Fix heading hierarchy with visually-hidden H1 - Add semantic
element - Use proper input types (tel, email) with autocomplete attributes - Add prefers-reduced-motion support - Fix typography: use proper ellipsis character (…) Performance (Vercel Best Practices): - Add defer to third-party scripts (pdfmake) - Add preconnect for CDN and font domains - Add font-variant-numeric: tabular-nums for number alignment - Replace transition:all with specific properties - Add touch-action: manipulation for mobile Design Enhancements: - Upgrade typography: DM Sans (body) + Plus Jakarta Sans (headings) - Add subtle noise texture overlay for depth - Add text-wrap: balance on headings SEO Improvements: - Add robots.txt with sitemap reference - Add sitemap.xml - Improve title tag with keywords (56 chars) - Add SoftwareApplication + WebApplication structured data - Add OG image meta tags - Change twitter:card to summary_large_image - Add theme-color meta tag Amp-Thread-ID: https://ampcode.com/threads/T-019bee5e-33fb-7526-9a97-ab10f03bd6ff Co-authored-by: Amp --- index.html | 125 +++++++++++++++++++++++-------- robots.txt | 8 ++ sitemap.xml | 9 +++ src/css/base/reset.css | 12 +++ src/css/base/typography.css | 17 ++++- src/css/components/buttons.css | 24 +++++- src/css/components/forms.css | 31 ++++++-- src/css/components/history.css | 18 ++++- src/css/components/invoice.css | 4 +- src/css/utilities/animations.css | 12 +++ src/css/utilities/helpers.css | 46 +++++++++--- src/js/main.js | 30 ++++++-- src/js/modules/history.js | 7 +- src/js/modules/items.js | 2 +- 14 files changed, 279 insertions(+), 66 deletions(-) create mode 100644 robots.txt create mode 100644 sitemap.xml diff --git a/index.html b/index.html index 3249b3d..7a4d4a8 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,10 @@ + - Rebill - Invoice Generator + Rebill - Free Invoice Generator | Create PDF Invoices Online @@ -16,35 +17,99 @@ - + + + + + - - + + + + + + + + + + + + + + + - - + + -
+ +
+

Rebill - Free Online Invoice Generator

+
+

Rebill - Invoice Generator Open Source

- +
@@ -61,20 +126,20 @@

Seller

- +
- +
- +
- +
@@ -84,20 +149,20 @@

Bill To

- +
- +
- +
- +
@@ -120,9 +185,9 @@

Invoice Details

- - - + + +
@@ -149,7 +214,7 @@

Line Items

- +
@@ -195,17 +260,15 @@

Import / Export

- +
- - - + + +
@@ -213,7 +276,7 @@

Import / Export

-
📄
+

Invoice Preview

Fill in the form on the left and click Generate Bill to see your invoice here.

@@ -278,7 +341,7 @@

- +
\ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..b2f6810 --- /dev/null +++ b/robots.txt @@ -0,0 +1,8 @@ +# Robots.txt for Rebill +# https://rebill.mrjl.dev + +User-agent: * +Allow: / + +# Sitemap location +Sitemap: https://rebill.mrjl.dev/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..69e38aa --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://rebill.mrjl.dev/ + 2026-01-24 + monthly + 1.0 + + diff --git a/src/css/base/reset.css b/src/css/base/reset.css index dea289c..33e206a 100644 --- a/src/css/base/reset.css +++ b/src/css/base/reset.css @@ -11,4 +11,16 @@ html { body { background: var(--background); min-height: 100vh; + position: relative; +} + +/* Subtle noise texture overlay for depth */ +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + opacity: 0.035; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); + z-index: 9999; } \ No newline at end of file diff --git a/src/css/base/typography.css b/src/css/base/typography.css index 99940bc..e6d6bad 100644 --- a/src/css/base/typography.css +++ b/src/css/base/typography.css @@ -1,7 +1,7 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap'); body { - font-family: 'Inter', 'Manrope', system-ui, -apple-system, sans-serif; + font-family: 'DM Sans', system-ui, -apple-system, sans-serif; font-size: 14px; line-height: 1.6; color: var(--text-primary); @@ -15,5 +15,18 @@ h3, h4, h5, h6 { + font-family: 'Plus Jakarta Sans', 'DM Sans', system-ui, sans-serif; color: var(--text-primary); + text-wrap: balance; +} + +/* Tabular numbers for invoice amounts */ +.invoice-table td, +.totals-preview span, +.item-total, +#displayBalanceDue, +#displaySubtotal, +#displayTaxAmount, +#displayDiscount { + font-variant-numeric: tabular-nums; } \ No newline at end of file diff --git a/src/css/components/buttons.css b/src/css/components/buttons.css index cd120e3..3fda9d9 100644 --- a/src/css/components/buttons.css +++ b/src/css/components/buttons.css @@ -11,12 +11,20 @@ button { align-items: center; justify-content: center; gap: var(--space-2); - transition: all var(--transition-base); - outline: none; + transition: + background-color var(--transition-base), + border-color var(--transition-base), + box-shadow var(--transition-base), + transform var(--transition-base), + color var(--transition-base); + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; } button:focus-visible { - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.4); + outline: 2px solid var(--primary-500); + outline-offset: 2px; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.25); } .button-row { @@ -52,7 +60,15 @@ button:focus-visible { color: var(--text-secondary); border: 1.5px solid var(--border-light); border-radius: var(--radius-md); - transition: all var(--transition-base); + transition: + background-color var(--transition-base), + border-color var(--transition-base), + color var(--transition-base); +} + +.file-input-label:focus-within { + outline: 2px solid var(--primary-500); + outline-offset: 2px; } .file-input-label:hover { diff --git a/src/css/components/forms.css b/src/css/components/forms.css index ceb329e..fc7d777 100644 --- a/src/css/components/forms.css +++ b/src/css/components/forms.css @@ -29,7 +29,7 @@ } .form-section h2 { - font-family: 'Manrope', sans-serif; + font-family: 'Plus Jakarta Sans', 'DM Sans', sans-serif; font-size: 1.5rem; font-weight: 800; color: var(--text-primary); @@ -65,7 +65,7 @@ } .form-block h3 { - font-family: 'Manrope', sans-serif; + font-family: 'Plus Jakarta Sans', 'DM Sans', sans-serif; font-size: 0.875rem; font-weight: 700; color: var(--primary-700); @@ -119,7 +119,13 @@ border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast); - outline: none; +} + +.form-group input:focus-visible, +.form-group textarea:focus-visible, +.form-group select:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 1px; } .form-group input::placeholder, @@ -205,7 +211,11 @@ .item-row input:focus { border-color: var(--primary-400); box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1); - outline: none; +} + +.item-row input:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 1px; } .item-total { @@ -232,7 +242,14 @@ font-weight: 600; line-height: 1; cursor: pointer; - transition: all var(--transition-fast); + transition: + background-color var(--transition-fast), + color var(--transition-fast); +} + +.remove-item:focus-visible { + outline: 2px solid var(--error-500); + outline-offset: 1px; } .remove-item:hover { @@ -275,7 +292,9 @@ font-size: 1rem; font-weight: 600; cursor: pointer; - transition: all var(--transition-fast); + transition: + background-color var(--transition-fast), + color var(--transition-fast); } .number-btn.decrement { diff --git a/src/css/components/history.css b/src/css/components/history.css index 59848e2..760c789 100644 --- a/src/css/components/history.css +++ b/src/css/components/history.css @@ -52,7 +52,14 @@ button.small { border-radius: var(--radius-sm); margin-bottom: var(--space-2); cursor: pointer; - transition: all var(--transition-fast); + transition: + border-color var(--transition-fast), + background-color var(--transition-fast); +} + +.history-item:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 2px; } .history-item:hover { @@ -98,11 +105,18 @@ button.small { border-radius: var(--radius-sm); font-size: 0.9rem; cursor: pointer; - transition: all var(--transition-fast); + transition: + background-color var(--transition-fast), + color var(--transition-fast); margin-left: var(--space-2); } .history-item-delete:hover { background: var(--error-50); color: var(--error-500); +} + +.history-item-delete:focus-visible { + outline: 2px solid var(--error-500); + outline-offset: 1px; } \ No newline at end of file diff --git a/src/css/components/invoice.css b/src/css/components/invoice.css index 3cb9c1e..1d0e451 100644 --- a/src/css/components/invoice.css +++ b/src/css/components/invoice.css @@ -20,7 +20,7 @@ } .placeholder-content h3 { - font-family: 'Manrope', sans-serif; + font-family: 'Plus Jakarta Sans', 'DM Sans', sans-serif; font-size: 1.25rem; font-weight: 700; color: var(--text-primary); @@ -43,7 +43,7 @@ padding: var(--space-10); height: 100vh; overflow-y: auto; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/src/css/utilities/animations.css b/src/css/utilities/animations.css index fdb8324..33f007c 100644 --- a/src/css/utilities/animations.css +++ b/src/css/utilities/animations.css @@ -9,4 +9,16 @@ opacity: 1; transform: translateY(0); } +} + +/* Respect reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } \ No newline at end of file diff --git a/src/css/utilities/helpers.css b/src/css/utilities/helpers.css index 64c754e..06d5fe3 100644 --- a/src/css/utilities/helpers.css +++ b/src/css/utilities/helpers.css @@ -1,5 +1,26 @@ /* ===== Helpers ===== */ +/* ===== Skip Link for Accessibility ===== */ +.skip-link { + position: absolute; + top: -100px; + left: 50%; + transform: translateX(-50%); + background: var(--primary-600); + color: var(--text-inverse); + padding: var(--space-3) var(--space-5); + border-radius: var(--radius-md); + font-weight: 600; + font-size: 0.875rem; + text-decoration: none; + z-index: 10000; + transition: top var(--transition-fast); +} + +.skip-link:focus { + top: var(--space-4); +} + /* ===== Open Source Badge ===== */ .open-source-badge { font-size: 0.6rem; @@ -11,7 +32,9 @@ text-decoration: none; margin-left: var(--space-2); vertical-align: middle; - transition: all var(--transition-fast); + transition: + background-color var(--transition-fast), + color var(--transition-fast); } .open-source-badge:hover { @@ -19,15 +42,20 @@ color: var(--primary-700); } -/* ===== Focus visible for accessibility ===== */ -:focus-visible { - outline: 2px solid var(--primary-400); +.open-source-badge:focus-visible { + outline: 2px solid var(--primary-500); outline-offset: 2px; } -input:focus-visible, -textarea:focus-visible, -select:focus-visible, -button:focus-visible { - outline: none; +/* ===== Visually Hidden (for screen readers) ===== */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } \ No newline at end of file diff --git a/src/js/main.js b/src/js/main.js index 2b31866..30e0168 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -224,7 +224,9 @@ function attachEventHandlers() { renderHistoryList(); }); - document.getElementById('historyList').addEventListener('click', (e) => { + const historyList = document.getElementById('historyList'); + + historyList.addEventListener('click', (e) => { const deleteBtn = e.target.closest('.history-item-delete'); if (deleteBtn) { e.stopPropagation(); @@ -240,6 +242,18 @@ function attachEventHandlers() { loadFromHistory(id); } }); + + // Keyboard support for history items (they have role="button") + historyList.addEventListener('keydown', (e) => { + if (e.key !== 'Enter' && e.key !== ' ') return; + + const item = e.target.closest('.history-item'); + if (item && !e.target.closest('.history-item-delete')) { + e.preventDefault(); + const id = parseInt(item.dataset.id, 10); + loadFromHistory(id); + } + }); } /** @@ -275,10 +289,12 @@ function init() { syncFromForm({ render: false, showInvoice: false }); } -// Expose functions to global scope for onclick handlers in HTML -window.generateBill = generateBill; -window.downloadPDF = downloadPDF; -window.shareInvoice = shareInvoice; - // Initialize on DOM ready -document.addEventListener('DOMContentLoaded', init); +document.addEventListener('DOMContentLoaded', () => { + init(); + + // Attach button handlers (moved from inline onclick) + document.getElementById('generateBillBtn').addEventListener('click', generateBill); + document.getElementById('downloadBtn').addEventListener('click', downloadPDF); + document.getElementById('shareBtn').addEventListener('click', shareInvoice); +}); diff --git a/src/js/modules/history.js b/src/js/modules/history.js index b1a63d6..c3d5f2f 100644 --- a/src/js/modules/history.js +++ b/src/js/modules/history.js @@ -101,7 +101,7 @@ export function renderHistoryList() { if (history.length === 0) { const empty = document.createElement('p'); empty.className = 'history-empty'; - empty.textContent = 'No saved invoices yet'; + empty.textContent = 'No saved invoices yet…'; list.appendChild(empty); return; } @@ -110,6 +110,9 @@ export function renderHistoryList() { const item = document.createElement('div'); item.className = 'history-item'; item.dataset.id = entry.id; + item.setAttribute('role', 'button'); + item.setAttribute('tabindex', '0'); + item.setAttribute('aria-label', `Load invoice #${entry.number} for ${entry.customerName || 'Unknown'}`); const info = document.createElement('div'); info.className = 'history-item-info'; @@ -129,7 +132,7 @@ export function renderHistoryList() { deleteBtn.type = 'button'; deleteBtn.className = 'history-item-delete'; deleteBtn.dataset.id = entry.id; - deleteBtn.title = 'Delete'; + deleteBtn.setAttribute('aria-label', `Delete invoice #${entry.number}`); deleteBtn.textContent = '×'; item.appendChild(info); diff --git a/src/js/modules/items.js b/src/js/modules/items.js index c4bd18b..28e80c0 100644 --- a/src/js/modules/items.js +++ b/src/js/modules/items.js @@ -60,7 +60,7 @@ export function addItemRow(item = {}) { const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'remove-item'; - removeBtn.title = 'Remove item'; + removeBtn.setAttribute('aria-label', 'Remove line item'); removeBtn.textContent = '×'; row.appendChild(descInput);