diff --git a/assets/common/design.css b/assets/common/design.css index 45db7c8..d201992 100644 --- a/assets/common/design.css +++ b/assets/common/design.css @@ -36,6 +36,7 @@ */ :root { + color-scheme: light; /* Ink, from strongest to faintest. Named by ROLE, so a screen never has to know which grey it is asking for. */ --ink: #0f172a; @@ -96,6 +97,7 @@ */ @media (prefers-color-scheme: dark) { :root { + color-scheme: dark; --ink: #f1f5f9; --ink-soft: #cbd5e1; --ink-faint: #94a3b8; diff --git a/assets/order-history/theme.css b/assets/order-history/theme.css new file mode 100644 index 0000000..aaa0640 --- /dev/null +++ b/assets/order-history/theme.css @@ -0,0 +1,89 @@ +/* Order history and every dialog it opens use paired surface/text colours. + design.css supplies the same light/dark palette as the menu. */ +:root { + color-scheme: light; + --light-bg: var(--ground); + --secondary-color: var(--ink); + --border-color: var(--line-strong); + --bs-body-color: var(--ink); + --bs-body-bg: var(--surface); + --bs-secondary-color: var(--ink-soft); + --bs-border-color: var(--line-strong); + --bs-light-rgb: 248, 250, 252; +} + +body { background: var(--ground); color: var(--ink); } +.modal-content { + --bs-modal-bg: var(--surface); + --bs-modal-color: var(--ink); + --bs-modal-border-color: var(--line-strong); + --bs-modal-header-border-color: var(--line-strong); + --bs-modal-footer-border-color: var(--line-strong); + background: var(--surface); + color: var(--ink); +} +.table { + --bs-table-color: var(--ink); + --bs-table-bg: var(--surface); + --bs-table-border-color: var(--line-strong); + --bs-table-striped-color: var(--ink); + --bs-table-striped-bg: var(--surface-sunk); + color: var(--ink); +} +.table > :not(caption) > * > * { color: var(--ink); border-color: var(--line-strong); } +.text-muted { color: var(--ink-soft) !important; } +.status-cancelled { background: #f8d7da; color: #842029; } +.order-items-table tfoot tr:last-child { border-top: 2px solid var(--line-strong) !important; } + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --bs-light-rgb: 13, 21, 38; + --bs-primary-rgb: 147, 197, 253; + --bs-success-rgb: 134, 239, 172; + --bs-danger-rgb: 252, 165, 165; + } + .mobile-header, .table-card:not(.all-tables), .order-card, + .search-filter-container, .product-suggestions, + #editItemNotesModal .modal-content { + background: var(--surface); + color: var(--ink); + border-color: var(--line-strong); + } + .order-item-card, .item-preview, .empty-state, .product-suggestion:hover { + background: var(--surface-sunk); + color: var(--ink); + border-color: var(--line-strong); + } + .screen-header p, .order-meta, .order-items-preview, .item-info .item-price, + .edit-notes-hint, .search-box i, .table-number, + .order-items-table .order-item-notes { color: var(--ink-soft); } + .modal h6, .modal .modal-title, .qty-display, .item-info h6, + #editOrderModal h6, #editOrderModal .modal-title, + #editItemNotesModal .edit-notes-header .modal-title { color: var(--ink); } + input:not([type=checkbox]):not([type=radio]), textarea, select, .form-control, + .qty-btn, .filter-btn:not(.active), .close-btn, .cancel-notes-btn, + #editOrderModal .modal-footer .close-btn { + color: var(--ink); + background-color: var(--surface-sunk); + border-color: var(--line-strong); + } + input::placeholder, textarea::placeholder { color: var(--ink-soft); opacity: 1; } + .form-control:focus, input:focus, textarea:focus, select:focus { + color: var(--ink); + background-color: var(--surface-sunk); + border-color: #93c5fd; + } + .modal-header, .modal-footer, .order-actions, .order-header, + #editOrderModal .modal-header { border-color: var(--line-strong); } + .btn-close { filter: invert(1) grayscale(100%) brightness(200%); opacity: .85; } + .text-dark { color: var(--ink) !important; } + .order-count { background: #16304e; color: #bfdbfe; } + .order-count.no-orders { background: #1e293b; color: #cbd5e1; } + .status-pending, .order-count.has-pending { background: #422d0b; color: #fde68a; } + .status-completed { background: #123c36; color: #a7f3d0; } + .status-cancelled { background: #491f2b; color: #fecdd3; } + .edit-btn, #editOrderModal .modal-footer .edit-btn { background: #1d4ed8; color: #fff; } + .cancel-btn, .remove-btn { background: #b91c1c; color: #fff; } + #page-loader { background: var(--ground); } +} diff --git a/order-history.html b/order-history.html index d997a20..ed3bfe4 100644 --- a/order-history.html +++ b/order-history.html @@ -4,6 +4,7 @@ + Order History - Restaurant @@ -59,8 +60,10 @@ + - + + diff --git a/tests/order-details-theme.test.js b/tests/order-details-theme.test.js new file mode 100644 index 0000000..9379b38 --- /dev/null +++ b/tests/order-details-theme.test.js @@ -0,0 +1,67 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium } = require('@playwright/test'); +const root = path.join(__dirname, '..'); +const screenshots = path.join(root, 'test-results', 'order-details-theme'); + +test('order details remain readable in both phone themes, including a live theme change', async () => { + const browser = await chromium.launch({ channel: process.env.CI ? undefined : 'msedge' }); + try { + const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await page.route('https://captain.test/**', async route => { + const name = new URL(route.request().url()).pathname.slice(1); + const file = path.resolve(root, name); + if (!file.startsWith(root + path.sep) || !fs.existsSync(file)) return route.abort(); + let body = fs.readFileSync(file); + if (name.endsWith('.html')) body = body.toString().replace(/]*>[\s\S]*?<\/script>/gi, ''); + await route.fulfill({ body, contentType: name.endsWith('.html') ? 'text/html' : name.endsWith('.css') ? 'text/css' : 'application/octet-stream' }); + }); + await page.goto('https://captain.test/order-history.html'); + const script = fs.readFileSync(path.join(root, 'assets/order-history/script.js'), 'utf8'); + const start = script.indexOf('function viewOrderDetails('); + const end = script.indexOf('\n// Edit order', start); + assert.ok(start >= 0 && end > start); + await page.addScriptTag({ content: fs.readFileSync(path.join(root, 'assets/common/money.js'), 'utf8') }); + const cancelledStart = script.indexOf('function lineIsCancelled('); + const struckStart = script.indexOf('function struck('); + const struckEnd = script.indexOf('\n}', struckStart) + 2; + await page.addScriptTag({ content: script.slice(cancelledStart, struckEnd) }); + await page.evaluate(source => { + window.currentOrderId = null; + window.formatDateTime = () => '01 Oct, 13:00'; + window.allOrders = [{ _id: 'sample', order_id: 'KOT-126', table_number: '8', status: 'pending', customer_name: 'Walk-in', person_count: 2, items: [{ name: 'Chicken Fried Rice', quantity: 1, price: 250, notes: 'Less spicy' }], tax: 12.5, total_amount: 262.5, discount_description: 'Serve together' }]; + (0, eval)(source); + window.viewOrderDetails('sample'); + document.getElementById('page-loader').remove(); + const modal = document.getElementById('orderDetailsModal'); + modal.classList.add('show'); + modal.style.display = 'block'; + }, script.slice(start, end)); + for (const colorScheme of ['light', 'dark', 'light']) { + await page.emulateMedia({ colorScheme }); + for (const selector of ['.modal-title', '.history-detail-context h2', '.history-detail-context p', '.history-detail-items strong', '.history-detail-items small', '.history-detail-totals dt', '.history-detail-totals dd', '.history-detail-note p', '#edit-order-btn', '.close-btn']) { + const contrast = await page.locator('#orderDetailsModal').locator(selector).first().evaluate(el => { + const rgb = value => value.match(/[\d.]+/g).slice(0, 3).map(Number); + const luminance = values => values.map(v => { v /= 255; return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4; }).reduce((n, v, i) => n + v * [.2126, .7152, .0722][i], 0); + const fg = luminance(rgb(getComputedStyle(el).color)); + let parent = el; + while (parent && ['rgba(0, 0, 0, 0)', 'transparent'].includes(getComputedStyle(parent).backgroundColor)) parent = parent.parentElement; + const bg = luminance(rgb(getComputedStyle(parent || document.body).backgroundColor)); + return (Math.max(fg, bg) + .05) / (Math.min(fg, bg) + .05); + }); + assert.ok(contrast >= 4.5, `${colorScheme} ${selector}: contrast ${contrast}`); + } + fs.mkdirSync(screenshots, { recursive: true }); + await page.screenshot({ path: path.join(screenshots, `order-details-${colorScheme}.png`), fullPage: true }); + } + await page.emulateMedia({ colorScheme: 'dark' }); + for (const status of ['completed', 'cancelled']) { + await page.evaluate(value => { allOrders[0].status = value; viewOrderDetails('sample'); }, status); + assert.equal(await page.locator('.history-detail-context p').nth(1).locator('span').textContent(), status); + assert.equal(await page.locator('#edit-order-btn').isVisible(), false); + } + } finally { await browser.close(); } +});