WFDASH-WEBAPP/public/pos/pos.html

615 lines
44 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
<meta name="theme-color" content="#ff385c">
<title>New sale</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="fa/all.min.css">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%23ff385c'/><text x='16' y='22' font-family='-apple-system,sans-serif' font-size='17' font-weight='700' text-anchor='middle' fill='white'>W</text></svg>">
<style>
:root{
--bg:#ffffff;--surface:#fff7f8;--fg:#222222;--muted:#dddddd;--border:#ffe3e8;--accent:#ff385c;
--ok:#0f8a3f;--warn:#b14a00;--danger:#c92a2a;
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
/* Text greys were hardcoded per-component, which is why the dark theme
had to restate 43 of them and why the greys drifted out of spec. One
token each; the dark block below rebinds them to lighter values so
they clear WCAG AA against a dark canvas. */
--muted-fg:#717171;--icon-fg:#888;--placeholder-fg:#666;
--shadow-up:0 -1px 0 var(--muted), 0 -8px 24px rgba(34,34,34,.06);
--font:'Inter',system-ui,-apple-system,Segoe UI,Helvetica Neue,Arial,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
/* Any class rule that sets `display` beats the UA stylesheet's
`[hidden]{display:none}`, so a hidden element stays visible and every
JS `.hidden = true/false` silently does nothing. `.cat-menu` and
`.cat-backdrop` already carry per-element overrides for this; this
one rule covers every element (and every future one) instead of
relying on someone remembering to add a twin. */
[hidden]{display:none !important}
html,body{height:100%;background:var(--bg);color:var(--fg);font:15px/1.45 var(--font);-webkit-font-smoothing:antialiased;overscroll-behavior:none;overflow-x:hidden}
body{display:flex;flex-direction:column;min-height:100dvh;max-width:480px;margin:0 auto;background:var(--bg);padding-bottom:env(safe-area-inset-bottom)}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
button:focus-visible,input:focus-visible,select:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(255,56,92,.30);border-radius:10px}
.topbar .back:focus-visible,.topbar .theme-btn:focus-visible{box-shadow:0 0 0 3px rgba(255,56,92,.30);border-radius:50%}
input{font:inherit;color:inherit}
/* ---- Topbar ---- */
.topbar{display:flex;align-items:center;gap:10px;padding:14px 16px 10px;background:var(--bg);position:sticky;top:0;z-index:10;border-bottom:1px solid transparent;transition:border-color .15s}
.topbar.scrolled{border-bottom-color:var(--muted)}
.topbar .back{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg)}
.topbar .back:active{background:var(--surface)}
.topbar h1{font-size:17px;font-weight:600;letter-spacing:-.01em;flex:1}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.topbar .netpill{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:var(--muted-fg);padding:4px 8px;border:1px solid var(--muted);border-radius:999px}
.topbar .netpill.off{color:var(--danger);border-color:var(--danger);background:#fff5f5}
/* ---- Search ---- */
.search{padding:6px 16px 10px;background:var(--bg);position:sticky;top:60px;z-index:9}
.search-box{display:flex;align-items:center;gap:10px;padding:11px 14px;border:1px solid var(--muted);border-radius:12px;background:#fff;box-shadow:0 1px 2px rgba(34,34,34,.02)}
.search-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.search-box svg{width:18px;height:18px;color:var(--muted-fg);flex:0 0 auto;stroke-width:1.7}
.search-box input{flex:1;border:0;outline:0;background:transparent;font-size:15px;min-width:0}
.search-box input::placeholder{color:#9a9a9a}
/* ---- Category dropdown ---- */
.cat-row{display:flex;align-items:center;gap:10px;padding:4px 16px 12px;background:var(--bg);position:sticky;top:118px;z-index:8}
.cat-trigger{display:flex;align-items:center;gap:8px;padding:10px 14px;border:1px solid var(--muted);border-radius:10px;background:#fff;font-size:14px;font-weight:500;color:var(--fg);min-height:42px;max-width:65%;letter-spacing:-.005em}
.cat-trigger:active{background:var(--surface)}
.cat-trigger .cat-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cat-trigger .cat-chev{width:16px;height:16px;flex:0 0 auto;color:var(--muted-fg);transition:transform .18s}
.cat-trigger[aria-expanded="true"] .cat-chev{transform:rotate(180deg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cat-count{font-size:12px;color:var(--muted-fg);letter-spacing:.01em;font-variant-numeric:tabular-nums}
.cat-menu{position:fixed;left:16px;right:16px;top:auto;background:#fff;border:1px solid var(--muted);border-radius:14px;box-shadow:0 12px 40px rgba(34,34,34,.18);z-index:60;max-width:448px;margin:0 auto;overflow:hidden;transform-origin:top center;animation:catIn .15s ease}
@keyframes catIn{from{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
.cat-menu[hidden]{display:none}
.cat-menu-head{max-height:60dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 0}
.cat-item{display:flex;align-items:center;gap:12px;padding:12px 16px;width:100%;text-align:left;font-size:14px;color:var(--fg);font-weight:500;letter-spacing:-.005em;border-bottom:1px solid var(--muted);background:none;min-height:44px}
.cat-item:last-child{border-bottom:0}
.cat-item:active{background:var(--surface)}
.cat-item .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cat-item .ct{font-size:12px;color:var(--muted-fg);font-variant-numeric:tabular-nums;letter-spacing:.02em;flex:0 0 auto}
.cat-item.active{background:var(--surface);color:var(--accent)}
.cat-item.active .ct{color:var(--accent)}
.cat-item.active::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--accent);flex:0 0 auto}
.cat-backdrop{position:fixed;inset:0;background:transparent;z-index:55}
.cat-backdrop[hidden]{display:none}
/* ---- Catalog list ---- */
.catalog{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 16px 220px}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.catalog .meta{padding:12px 0 6px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-fg);display:flex;justify-content:space-between}
.catalog .empty{padding:60px 20px;text-align:center;color:#9a9a9a}
.catalog .more{display:flex;flex-direction:column;align-items:center;gap:2px;width:100%;margin:14px 0 8px;padding:14px;border:1px solid var(--muted);border-radius:12px;background:var(--surface-card);color:var(--fg);font-size:14px;font-weight:600;letter-spacing:-.005em}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.catalog .more small{font-size:11px;font-weight:500;color:var(--muted-fg);letter-spacing:.02em}
.catalog .more:active{background:var(--surface)}
.catalog .empty h3{font-size:16px;font-weight:600;color:var(--fg);margin-bottom:6px;letter-spacing:-.01em}
.catalog .empty p{font-size:13px;line-height:1.5}
.prod{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--muted);min-height:60px;background:none;border-left:0;border-right:0;border-top:0;width:100%;text-align:left}
.prod:active{background:var(--surface)}
.prod .ic{width:40px;height:40px;border-radius:10px;background:var(--surface);border:1px solid var(--border);display:grid;place-items:center;color:var(--accent);font-size:12px;font-weight:700;letter-spacing:.04em;flex:0 0 auto}
.prod .ic .ic-fa{font-size:18px;line-height:1;color:var(--accent)}
.prod .body{flex:1;min-width:0;padding-right:8px}
.prod .name{font-size:14px;font-weight:500;line-height:1.3;letter-spacing:-.005em;color:var(--fg);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.prod .sub{font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prod .price{font-size:14px;font-weight:600;letter-spacing:-.005em;color:var(--fg);font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.prod .price small{font-size:11px;color:var(--muted-fg);font-weight:500;margin-left:2px}
/* The quantity sits under the price, not in the .sub line: .sub is
nowrap + ellipsis, so a long SKU truncated it and the number — and the
in-cart count beside it — fell off the right edge. */
.prod .stock{display:block;font-size:11px;font-weight:500;letter-spacing:.01em;color:var(--muted-fg);margin-top:2px;white-space:nowrap}
/* Out of stock: the quantity is not positive. The cue is informational —
the item still sells (the till oversells on purpose) — so it only
tints the number. The number itself is never hidden or clamped. */
.prod.oos .stock{color:var(--danger)}
.prod.oos .name{opacity:.72}
/* ---- Cart bottom sheet ---- */
.cart-bar{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--muted);padding:12px 16px calc(12px + env(safe-area-inset-bottom));z-index:20;display:flex;align-items:center;gap:12px;box-shadow:var(--shadow-up);max-width:480px;margin:0 auto;transform:translateY(100%);transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.cart-bar.show{transform:translateY(0)}
.cart-bar .count{display:flex;flex-direction:column;line-height:1.15}
.cart-bar .count b{font-size:20px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cart-bar .count span{font-size:11px;color:var(--muted-fg);letter-spacing:.04em;text-transform:uppercase}
.cart-bar .total{margin-left:auto;font-size:20px;font-weight:700;letter-spacing:-.02em;color:var(--fg);font-variant-numeric:tabular-nums}
.cart-bar .checkout{padding:14px 22px;background:var(--accent);color:#fff;border-radius:12px;font-weight:600;font-size:15px;letter-spacing:-.005em;min-height:48px;display:flex;align-items:center;gap:6px}
.cart-bar .checkout:active{background:#e5324f}
.cart-bar .checkout svg{width:18px;height:18px;stroke-width:2}
/* ---- Cart drawer ---- */
.drawer{position:fixed;inset:0;background:rgba(34,34,34,.5);z-index:50;opacity:0;pointer-events:none;transition:opacity .2s}
.drawer.show{opacity:1;pointer-events:auto}
.sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:18px 18px 0 0;max-height:88dvh;display:flex;flex-direction:column;transform:translateY(100%);transition:transform .25s cubic-bezier(.2,.8,.2,1);max-width:480px;margin:0 auto;padding-bottom:env(safe-area-inset-bottom)}
.drawer.show .sheet{transform:translateY(0)}
.sheet-head{display:flex;align-items:center;padding:18px 20px 8px;border-bottom:1px solid var(--muted)}
.sheet-head h2{font-size:18px;font-weight:700;letter-spacing:-.01em;flex:1}
.sheet-head .x{width:32px;height:32px;border-radius:50%;background:var(--surface);display:grid;place-items:center;color:var(--fg)}
.sheet-body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 0}
.line{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--muted)}
.line .nm{flex:1;min-width:0;font-size:14px;font-weight:500;line-height:1.3;letter-spacing:-.005em}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.line .nm small{display:block;font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em}
.qty{display:flex;align-items:center;border:1px solid var(--muted);border-radius:10px;overflow:hidden}
.qty button{width:34px;height:34px;display:grid;place-items:center;color:var(--fg);font-size:16px;font-weight:600}
.qty button:active{background:var(--surface)}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
.qty .v{min-width:44px;padding:6px 2px;text-align:center;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--fg);border-radius:6px}
.qty .v:active{background:var(--surface)}
.line .pr{width:78px;text-align:right;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--fg)}
.line .rm{width:32px;height:32px;border-radius:50%;color:#9a9a9a;display:grid;place-items:center}
.line .rm:active{background:#fff0f0;color:var(--danger)}
.sheet-foot{padding:14px 20px 18px;border-top:1px solid var(--muted);background:#fff}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.totals{display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:var(--muted-fg);letter-spacing:.01em}
.totals b{color:var(--fg);font-variant-numeric:tabular-nums}
.totals.grand{padding-top:12px;margin-top:8px;border-top:1px solid var(--muted);font-size:17px;color:var(--fg);font-weight:700;letter-spacing:-.01em}
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
/* ---- Ad-hoc (custom) line ---- */
/* A dashed outline rather than a filled button: this is a secondary
action next to the real catalogue, and it should read as "something
extra" rather than compete with the checkout CTA below it. */
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.custom-item-btn{width:100%;margin-bottom:12px;padding:10px 12px;border:1px dashed #c9c9c9;border-radius:9px;background:none;color:var(--muted-fg);font-size:13px;font-weight:600;letter-spacing:.01em;text-align:center;cursor:pointer}
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
.custom-item-btn:active{background:#f4f4f4}
/* Ties a custom line back to the "not stock" meaning at a glance, and
stops anyone mistaking it for a catalogue item on a stocktake. */
.custom-line .nm{color:#8a6d3b}
.custom-line .nm small{color:#a08a5e}
/* ---- Discount block ---- */
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
/* The trigger rides INSIDE the Subtotal row (see the markup below), so it
adds no vertical height at all — the footer keeps every pixel it had
before. It stays visible rather than collapsing away, because a discount
control that appears from nowhere is one nobody finds under pressure. */
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.discount-trigger{display:inline-flex;align-items:center;gap:3px;margin-left:8px;padding:3px 6px;border:0;border-radius:6px;background:none;color:var(--muted-fg);font-size:12px;font-weight:600;letter-spacing:.01em;vertical-align:middle}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
.discount-trigger:active{background:var(--surface);color:var(--fg)}
.discount-trigger svg{width:13px;height:13px;color:inherit;transition:transform .18s}
.discount-trigger[aria-expanded="true"] svg{transform:rotate(180deg)}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
.discount-trigger.has-value{color:var(--accent)}
.discount-trigger.has-value svg{color:var(--accent)}
.discount-body{padding:12px 0 4px;display:flex;flex-direction:column;gap:10px}
.discount-pills{display:flex;gap:6px;padding:3px;background:var(--surface);border:1px solid var(--muted);border-radius:999px;width:fit-content}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.discount-pill{padding:7px 16px;border-radius:999px;font-size:13px;font-weight:600;letter-spacing:-.005em;color:var(--muted-fg);min-height:34px}
.discount-pill.active{background:#fff;color:var(--accent);box-shadow:0 1px 2px rgba(34,34,34,.06)}
.discount-input-row{display:flex;align-items:center;gap:8px}
.discount-input-row input{flex:1 1 0;min-width:0;padding:11px 12px;border:1px solid var(--muted);border-radius:10px;font-size:15px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff}
.discount-input-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.discount-suffix{font-size:13px;font-weight:600;color:var(--muted-fg);width:18px;text-align:left;letter-spacing:-.005em}
.discount-apply{padding:11px 16px;background:var(--accent);color:#fff;border-radius:10px;font-size:13px;font-weight:600;letter-spacing:-.005em;min-height:42px;min-width:78px}
.discount-apply:active{background:#e5324f}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.discount-hint{font-size:11px;color:var(--muted-fg);margin:0;letter-spacing:.01em}
.totals.discount-line{color:var(--danger)}
.totals.discount-line b{color:var(--danger)}
/* ---- Edit line modal (long-press) ---- */
.edit-backdrop{position:fixed;inset:0;background:rgba(34,34,34,.5);z-index:70;opacity:0;pointer-events:none;transition:opacity .15s}
.edit-backdrop.show{opacity:1;pointer-events:auto}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
/* The sheet is a SIBLING of the backdrop, not a child, so it cannot be
styled via `.edit-backdrop.show .edit-sheet` — that selector never
matched and the sheet stayed at opacity:0 forever, which showed the
operator a grey screen with no dialog on it. It needs its own stacking
above the backdrop (z-index 70) and its own .show rule. Keep them
siblings: nesting the sheet inside the backdrop would make every tap on
the sheet bubble to the backdrop's onclick and close it. */
.edit-sheet{position:fixed;left:50%;bottom:24px;z-index:71;transform:translate(-50%, 24px);background:#fff;border-radius:18px;width:calc(100% - 32px);max-width:448px;padding:18px 18px 14px;opacity:0;transition:opacity .15s, transform .22s cubic-bezier(.2,.8,.2,1);box-shadow:0 20px 60px rgba(34,34,34,.30);pointer-events:none;display:flex;flex-direction:column;gap:12px;max-height:80dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding-bottom:calc(14px + env(safe-area-inset-bottom))}
.edit-sheet.show{opacity:1;transform:translate(-50%, 0);pointer-events:auto}
.edit-sheet h3{font-size:15px;font-weight:600;letter-spacing:-.01em;margin:0;display:flex;align-items:center;gap:8px}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.edit-sheet h3 small{font-size:11px;font-weight:500;color:var(--muted-fg);letter-spacing:.02em;margin-left:auto}
.edit-row{display:flex;flex-direction:column;gap:6px}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.edit-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-fg)}
.edit-row input{width:100%;box-sizing:border-box;padding:12px 12px;border:1px solid var(--muted);border-radius:10px;font-size:16px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff}
.edit-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
.edit-actions{display:flex;gap:8px;margin-top:4px}
.edit-actions button{flex:1;padding:13px;border-radius:12px;font-size:14px;font-weight:600;letter-spacing:-.005em;min-height:46px}
.edit-actions .primary{background:var(--accent);color:#fff}
.edit-actions .primary:active{background:#e5324f}
.edit-actions .secondary{background:#fff;border:1px solid var(--muted);color:var(--fg)}
.edit-actions .secondary:active{background:var(--surface)}
.edit-actions .danger{background:#fff;border:1px solid #f0c0c0;color:var(--danger)}
.edit-actions .danger:active{background:#fff0f0}
/* Long-press hint chip — shown briefly when a line is held long
enough to count down to the modal opening, so the operator
knows the long-press did something. */
.line-holding{background:var(--surface);transition:background .15s}
/* ---- Payment sheet ---- */
.pay-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:8px 20px 16px}
.pay-tile{padding:18px 14px;border:1.5px solid var(--muted);border-radius:14px;background:#fff;text-align:left;min-height:92px;display:flex;flex-direction:column;justify-content:space-between}
.pay-tile:active{background:var(--surface)}
.pay-tile .lab{font-size:14px;font-weight:600;letter-spacing:-.01em}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.pay-tile .sub{font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em}
.pay-tile .ic{width:32px;height:32px;border-radius:8px;background:var(--surface);color:var(--accent);display:grid;place-items:center;margin-bottom:8px}
.pay-tile .ic svg{width:18px;height:18px;stroke-width:1.7}
.pay-tile.selected{border-color:var(--accent);background:#fff8fa}
.pay-tile.selected .ic{background:var(--accent);color:#fff}
.cash-block{padding:0 16px 20px;display:none}
.cash-block.show{display:block}
.cash-row{display:flex;gap:8px;margin-bottom:10px;min-width:0}
.cash-row .field{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cash-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-fg);margin-bottom:4px}
.cash-row input{width:100%;box-sizing:border-box;padding:12px 10px;border:1px solid var(--muted);border-radius:10px;font-size:16px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff;min-width:0}
.cash-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
.quick{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
.quick button{padding:9px 13px;border:1px solid var(--muted);border-radius:999px;font-size:13px;font-weight:500;background:#fff;color:var(--fg);min-height:36px}
.quick button:active{background:var(--surface);border-color:#bbb}
.change-out{padding:14px;border-radius:12px;background:var(--surface);border:1px solid var(--border);text-align:center;min-height:64px;display:flex;flex-direction:column;justify-content:center;opacity:0;max-height:0;padding-top:0;padding-bottom:0;overflow:hidden;transition:opacity .15s, max-height .2s, padding .2s, border-width .2s;border-width:0}
.change-out.show{opacity:1;max-height:120px;padding:14px;border-width:1px}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.change-out .lbl{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-fg)}
.change-out .amt{font-size:32px;font-weight:700;letter-spacing:-.02em;color:var(--ok);font-variant-numeric:tabular-nums;margin-top:4px}
.change-out.short .amt{color:var(--danger)}
.cust-block{padding:0 16px 16px;display:none}
.cust-block.show{display:block}
.cust-block .search-row{display:flex;gap:8px;margin-bottom:10px;min-width:0}
.cust-block input{flex:1 1 0;width:100%;box-sizing:border-box;min-width:0;padding:12px 10px;border:1px solid var(--muted);border-radius:10px;font-size:14px;outline:0;background:#fff}
.cust-block input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
.cust-results{border:1px solid var(--muted);border-radius:10px;max-height:160px;overflow-y:auto;background:#fff}
.cust-results:empty{display:none}
.cust-results div{padding:10px 14px;border-bottom:1px solid var(--muted);font-size:14px;display:flex;align-items:center;gap:8px;cursor:pointer}
.cust-results div:last-child{border-bottom:0}
.cust-results div:active{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cust-results small{color:var(--muted-fg);font-size:11px;margin-left:auto;letter-spacing:.02em}
feat(pos): customer picker matches the company name, and stops hiding truncation The account-sale picker filtered on three customer columns — name, number, email. `company` on a customer row is a RELATION id, not a name, so it could never be text-matched; the company name was only ever resolved afterwards by `expand` for display. Typing "Sibanye" returned nothing even though 52 people work there. - the filter now traverses the relation: `company.company ~ q`. Verified live: "Sibanye - Rustenburg" 0 → 52, "Adal" 0 → 2, "Alano" 0 → 1, while "Botha" and "Smit" are unchanged at 6 and 4. - each row carries `viaCompany`, and name matches sort above company-only matches. PB's own sort is by name across the whole result set, which would interleave the two bands. - /customers returns { items, total, shown } and the till renders a count header: "52 matches", or "Showing 100 of 122 — keep typing to narrow". The old `slice(0, 8)` showed 8 of 52 people with nothing to suggest 44 were missing; 15 companies have more staff than that. The picker click handler bound to every `div` in the results box, so adding a count header would have made it selectable and picked a customer with an empty id. Now scoped to `.cust-row`. Two bugs the tests caught rather than the code: - the display cap was first set to 50 while its own comment claimed it sat above the largest company — which has 52 staff. That silently re-created the truncation this change removes. Now 100, with a test asserting a 52-person company comes back whole. - PocketBase has no escape for a literal backslash: `\'` is accepted, but one, two or three backslashes all fail to parse and 500 the request, so typing `\` in the search box errored. No escaping survives, so backslashes are stripped before the filter (`M20\Nut` searches `M20Nut`) and a backslash-only query returns nothing rather than matching every customer. 13 new tests cover the company clause, the 52-staff case, the cap, ranking order, backslash stripping and the empty-after-strip case. Verified live against PocketBase across 10 queries — the ranking guarantee held on every one. sw.js v33 -> v34: pos-app.js and pos.html are cache-first in the shell.
2026-10-07 13:53:23 +00:00
/* Count header + empty state. Both are divs inside .cust-results, so they
need the row cursor/padding neutralised explicitly. */
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
.cust-results .cust-count{padding:7px 14px;background:var(--surface);border-bottom:1px solid var(--muted);font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--muted-fg);cursor:default}
.cust-results .cust-none{padding:11px 14px;font-size:13px;color:var(--muted-fg);cursor:default}
feat(pos): customer picker matches the company name, and stops hiding truncation The account-sale picker filtered on three customer columns — name, number, email. `company` on a customer row is a RELATION id, not a name, so it could never be text-matched; the company name was only ever resolved afterwards by `expand` for display. Typing "Sibanye" returned nothing even though 52 people work there. - the filter now traverses the relation: `company.company ~ q`. Verified live: "Sibanye - Rustenburg" 0 → 52, "Adal" 0 → 2, "Alano" 0 → 1, while "Botha" and "Smit" are unchanged at 6 and 4. - each row carries `viaCompany`, and name matches sort above company-only matches. PB's own sort is by name across the whole result set, which would interleave the two bands. - /customers returns { items, total, shown } and the till renders a count header: "52 matches", or "Showing 100 of 122 — keep typing to narrow". The old `slice(0, 8)` showed 8 of 52 people with nothing to suggest 44 were missing; 15 companies have more staff than that. The picker click handler bound to every `div` in the results box, so adding a count header would have made it selectable and picked a customer with an empty id. Now scoped to `.cust-row`. Two bugs the tests caught rather than the code: - the display cap was first set to 50 while its own comment claimed it sat above the largest company — which has 52 staff. That silently re-created the truncation this change removes. Now 100, with a test asserting a 52-person company comes back whole. - PocketBase has no escape for a literal backslash: `\'` is accepted, but one, two or three backslashes all fail to parse and 500 the request, so typing `\` in the search box errored. No escaping survives, so backslashes are stripped before the filter (`M20\Nut` searches `M20Nut`) and a backslash-only query returns nothing rather than matching every customer. 13 new tests cover the company clause, the 52-staff case, the cap, ranking order, backslash stripping and the empty-after-strip case. Verified live against PocketBase across 10 queries — the ranking guarantee held on every one. sw.js v33 -> v34: pos-app.js and pos.html are cache-first in the shell.
2026-10-07 13:53:23 +00:00
.cust-results .cust-count:active,.cust-results .cust-none:active{background:none}
.cust-picked{padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:13px;display:none;align-items:center;gap:8px;margin-bottom:8px}
.cust-picked.show{display:flex}
.cust-picked b{flex:1;font-weight:600}
.cust-picked button{color:var(--danger);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
.final-cta{width:calc(100% - 40px);margin:0 20px 18px;padding:16px;background:var(--accent);color:#fff;border-radius:14px;font-size:16px;font-weight:600;letter-spacing:-.01em;min-height:54px;display:flex;align-items:center;justify-content:center;gap:8px}
.final-cta:active{background:#e5324f}
.final-cta:disabled{background:#e0e0e0;color:#999}
.final-cta svg{width:20px;height:20px;stroke-width:2}
/* ---- theme toggle ---- */
.theme-btn{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg);flex:0 0 auto;background:transparent;border:0;cursor:pointer;position:relative}
.theme-btn:active{background:var(--surface)}
.theme-btn svg{width:18px;height:18px}
.theme-btn .i-moon{display:none}
:root[data-theme="dark"] .theme-btn .i-sun{display:none}
:root[data-theme="dark"] .theme-btn .i-moon{display:block}
:root[data-theme="auto"] .theme-btn .i-moon{display:none}
:root[data-theme="auto"] .theme-btn .i-sun{display:block}
/* ---- dark theme (auto follows prefers-color-scheme) ---- */
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
/* Dark ramp. The previous canvas was #0f0f0f (L* 4.3 — effectively black)
with surfaces only 3-4 L* above it, and mixed a warm #1c1416 against a
neutral #181818 in the same UI. That near-black canvas plus two
competing surface tones is what read as "muddy". This is a single
cool-neutral ramp with four real steps, and the text greys are lifted
so the most-used one clears WCAG AA (was 3.93:1, now 6.49:1). */
:root[data-theme="dark"]{
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
--bg:#161619; /* L* 7.4 — lifted off black */
--surface:#1e1e23; /* L* 11.5 — cards, drawers, fields */
--raised:#26262d; /* L* 15.4 — menus and modals sit above surfaces */
--fg:#f2f2f5;
--muted:#303039; /* L* 20.2 — separators */
--border:#3b3b46; /* L* 25.3 — outlines */
--muted-fg:#9a9aa6; /* secondary text 6.49:1 on --bg (AA) */
--icon-fg:#8e8e9a; /* icons / labels 5.58:1 on --bg (AA) */
--placeholder-fg:#7c7c88; /* placeholder 4.38:1 */
--shadow-up:0 -1px 0 var(--muted), 0 -8px 24px rgba(0,0,0,.4);
}
:root[data-theme="dark"] body{background:var(--bg);color:var(--fg)}
:root[data-theme="dark"] .topbar{background:var(--bg);border-bottom-color:var(--muted)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .search-box{background:var(--surface);border-color:var(--muted)}
:root[data-theme="dark"] .search-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .search-box svg{color:var(--icon-fg)}
:root[data-theme="dark"] .search-box input{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .search-box input::placeholder{color:var(--placeholder-fg)}
:root[data-theme="dark"] .chip{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .chip.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cat-trigger{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .cat-trigger .cat-chev{color:var(--icon-fg)}
:root[data-theme="dark"] .cat-count{color:var(--icon-fg)}
:root[data-theme="dark"] .cat-menu{background:var(--raised);border-color:var(--muted);box-shadow:0 12px 40px rgba(0,0,0,.5)}
:root[data-theme="dark"] .cat-item{color:var(--fg);border-bottom-color:var(--muted)}
:root[data-theme="dark"] .cat-item:active{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cat-item .ct{color:var(--icon-fg)}
:root[data-theme="dark"] .cat-item.active{background:var(--surface);color:var(--accent)}
:root[data-theme="dark"] .prod{border-bottom-color:var(--muted)}
:root[data-theme="dark"] .prod:active{background:var(--surface)}
:root[data-theme="dark"] .prod .ic{background:var(--surface);border-color:var(--border);color:var(--accent)}
:root[data-theme="dark"] .prod .name{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .prod .sub{color:var(--icon-fg)}
:root[data-theme="dark"] .prod .price{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .prod .price small{color:var(--icon-fg)}
:root[data-theme="dark"] .prod .stock{color:var(--icon-fg)}
/* The dark theme restates `.prod .stock` at a higher specificity than
`.prod.oos .stock`, so the out-of-stock colour needs a dark rule of
its own or it never shows. `#ff6680` is the dark danger colour
this file already uses (.totals.discount-line, .edit-actions .danger);
--danger itself is not rebound for dark and would not clear AA on the
dark canvas. */
:root[data-theme="dark"] .prod.oos .stock{color:#ff6680}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cart-bar{background:var(--surface);border-top-color:var(--muted);box-shadow:var(--shadow-up)}
:root[data-theme="dark"] .cart-bar .count span{color:var(--icon-fg)}
:root[data-theme="dark"] .cart-bar .total{color:var(--fg)}
:root[data-theme="dark"] .drawer{background:rgba(0,0,0,.7)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .sheet{background:var(--surface)}
:root[data-theme="dark"] .sheet-head{border-bottom-color:var(--muted)}
:root[data-theme="dark"] .sheet-head .x{background:var(--surface);color:var(--fg)}
:root[data-theme="dark"] .line{border-bottom-color:var(--muted)}
:root[data-theme="dark"] .line .nm{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .line .nm small{color:var(--icon-fg)}
:root[data-theme="dark"] .qty{border-color:var(--muted)}
:root[data-theme="dark"] .qty button{color:var(--fg)}
:root[data-theme="dark"] .qty button:active{background:var(--surface)}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
:root[data-theme="dark"] .qty .v{color:var(--fg)}
:root[data-theme="dark"] .line .pr{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .line .rm{color:var(--placeholder-fg)}
:root[data-theme="dark"] .line .rm:active{background:#3a2429;color:var(--accent)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .sheet-foot{background:var(--surface);border-top-color:var(--muted)}
:root[data-theme="dark"] .totals{color:var(--icon-fg)}
:root[data-theme="dark"] .totals b{color:var(--fg)}
:root[data-theme="dark"] .totals.grand{border-top-color:var(--muted);color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .discount-trigger{color:var(--icon-fg)}
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
:root[data-theme="dark"] .custom-item-btn{border-color:#3a3a3a;color:#999}
:root[data-theme="dark"] .custom-item-btn:active{background:#222}
:root[data-theme="dark"] .custom-line .nm{color:#d2b47c}
:root[data-theme="dark"] .custom-line .nm small{color:#a08a5e}
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
:root[data-theme="dark"] .discount-trigger:active{color:var(--fg)}
:root[data-theme="dark"] .discount-trigger.has-value{color:#ff6680}
:root[data-theme="dark"] .discount-pills{background:#222;border-color:var(--muted)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .discount-pill{color:var(--icon-fg)}
:root[data-theme="dark"] .discount-pill.active{background:var(--surface);color:#ff6680;box-shadow:0 1px 2px rgba(0,0,0,.4)}
:root[data-theme="dark"] .discount-input-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .discount-input-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .discount-suffix{color:var(--icon-fg)}
:root[data-theme="dark"] .discount-hint{color:var(--icon-fg)}
:root[data-theme="dark"] .totals.discount-line{color:#ff6680}
:root[data-theme="dark"] .totals.discount-line b{color:#ff6680}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .empty{color:var(--placeholder-fg)}
:root[data-theme="dark"] .empty h3{color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .empty p{color:var(--icon-fg)}
:root[data-theme="dark"] .netpill{background:transparent;color:var(--icon-fg);border-color:var(--muted)}
:root[data-theme="dark"] .netpill.off{color:#ff6680;border-color:#ff6680;background:#2a1a1a}
:root[data-theme="dark"] .back:active{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .pay-tile{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .pay-tile:active{background:var(--surface)}
:root[data-theme="dark"] .pay-tile .ic{background:var(--surface);color:var(--accent)}
:root[data-theme="dark"] .pay-tile.selected{border-color:var(--accent);background:#2a1a1a}
:root[data-theme="dark"] .pay-tile.selected .ic{background:var(--accent);color:#fff}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cash-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .cash-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
:root[data-theme="dark"] .cash-row input[readonly]{background:#222;color:#aaa}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cash-row label{color:var(--icon-fg)}
:root[data-theme="dark"] .quick button{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .quick button:active{background:var(--surface);border-color:#444}
:root[data-theme="dark"] .change-out{background:var(--surface);border-color:var(--border);color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .change-out .lbl{color:var(--icon-fg)}
:root[data-theme="dark"] .cust-block input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .cust-block input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cust-results{background:var(--surface);border-color:var(--muted)}
:root[data-theme="dark"] .cust-results div{border-bottom-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .cust-results div:active{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .cust-results small{color:var(--icon-fg)}
:root[data-theme="dark"] .cust-results .cust-count{background:#222;border-bottom-color:var(--muted);color:var(--icon-fg)}
:root[data-theme="dark"] .cust-results .cust-none{color:var(--icon-fg)}
:root[data-theme="dark"] .cust-picked{background:var(--surface);border-color:var(--border);color:var(--fg)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .edit-sheet{background:var(--raised);box-shadow:0 20px 60px rgba(0,0,0,.6)}
:root[data-theme="dark"] .edit-sheet h3 small{color:var(--icon-fg)}
:root[data-theme="dark"] .edit-row label{color:var(--icon-fg)}
:root[data-theme="dark"] .edit-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .edit-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .edit-actions .secondary{background:var(--surface);border-color:var(--muted);color:var(--fg)}
:root[data-theme="dark"] .edit-actions .secondary:active{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .edit-actions .danger{background:var(--surface);border-color:#5a2429;color:#ff6680}
:root[data-theme="dark"] .edit-actions .danger:active{background:#2a1a1a}
:root[data-theme="dark"] .line-holding{background:var(--surface)}
fix(pos): dead discount toggle + stale discount figure, lift dark palette Two reported symptoms that turned out to be one bug, plus the dark mode the user described as "too black". The `hidden` attribute does nothing on the discount controls `hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`. Any author rule that sets `display` outranks it — and `.totals` and `.discount-body` both set `display:flex`. So `el.hidden = true` was a no-op on both: - the discount tier panel could not be collapsed, making "Add discount" look like a dead control; - the Discount totals row stayed on screen after the discount was cleared, still showing the last figure (R 98.37) under a total that no longer contained it. The JS only ever set `hidden` and never cleared the text, so the operator had no way to tell stale text from a live one. `.cat-menu` and `.cat-backdrop` already carried per-element `[hidden]{display:none}` overrides for exactly this reason; the two discount elements were missed. Replaced with one global rule so the whole class is covered rather than relying on each element to remember. The clear path now also blanks the amount. Verified statically rather than in a browser: jsdom does not model the UA-vs-author cascade and reported the broken and fixed cases identically, so the cascade was checked by resolving which rules apply. Regression test fails without the fix. Dark palette The POS canvas was #0f0f0f (L* 4.3 — effectively black) with surfaces only 3-4 L* above it, mixing a warm #1c1416 against a neutral #181818 in the same UI. That near-black base plus two competing surface tones is what read as muddy. Replaced with a single cool-neutral ramp with four real steps, and menus/modals now sit on a `--raised` layer instead of reusing the surface colour. `#717171` was the secondary text colour in 25 places and measured 3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1. Both the text greys and the surfaces were hardcoded per component across seven POS pages; they are now tokens, with the dark block rebinding them. The shared `.dark` block in themes.css (drives all 24 themes) sat at oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still above AA. Cursor/Supabase, which override it darker, were at L* 1.3 and lifted too. Service worker cache bumped to pos-v36 — pos.html and pos-app.js are served cache-first, so without the bump the tills keep serving the broken files. 3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
:root[data-theme="dark"] .final-cta:disabled{background:#2a2a2a;color:var(--placeholder-fg)}
@media (prefers-reduced-motion: reduce){*{transition-duration:0.01ms !important;animation-duration:0.01ms !important}}
</style>
</head>
<body>
<header class="topbar" id="topbar">
<button class="back" data-od-id="back-btn" onclick="location.href='index.html'" aria-label="Back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 6l-6 6 6 6"/></svg>
</button>
<h1>New sale</h1>
<span class="netpill" id="netpill">…</span>
<button class="theme-btn" data-theme-toggle data-od-id="theme-toggle" aria-label="Toggle theme" title="Toggle theme" onclick="(window.WFTheme&&window.WFTheme.cycle())||(window.__wfCycle&&window.__wfCycle())">
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
</header>
<div class="search">
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="q" type="search" placeholder="Search SKU or product" autocomplete="off" inputmode="search" enterkeyhint="search">
</div>
</div>
<div class="cat-row">
<button class="cat-trigger" id="catTrigger" data-od-id="cat-trigger" aria-haspopup="listbox" aria-expanded="false">
<span class="cat-label" id="catLabel">All categories</span>
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>
</button>
<span class="cat-count" id="catCount">— products</span>
</div>
<div class="cat-menu" id="catMenu" role="listbox" hidden>
<div class="cat-menu-head" id="catMenuItems"></div>
</div>
<div class="cat-backdrop" id="catBackdrop" hidden></div>
<main class="catalog" id="catalog">
<div class="empty" id="empty">
<h3>Loading catalog…</h3>
<p>Fetching products from the register.</p>
</div>
</main>
<div class="cart-bar" id="cartBar" data-od-id="cart-bar">
<button class="back" style="width:44px;height:44px;border-radius:50%;background:var(--surface);border:1px solid var(--border)" onclick="openDrawer()" aria-label="View cart">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 6h2l2 12h12l2-9H6"/><circle cx="9" cy="20" r="1.4"/><circle cx="17" cy="20" r="1.4"/></svg>
</button>
<div class="count"><b id="cartCount">0</b><span>items</span></div>
<div class="total" id="cartTotal">R 0.00</div>
<button class="checkout" data-od-id="checkout-btn" onclick="openPayment()">
Charge
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</button>
</div>
<!-- Cart drawer -->
<div class="drawer" id="cartDrawer">
<div class="sheet">
<div class="sheet-head">
<h2>Cart</h2>
<button class="x" onclick="closeDrawer()" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
<div class="sheet-body" id="cartLines"></div>
<div class="sheet-foot">
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<!-- Ad-hoc line: anything the customer asks for that isn't a
catalogue item (a cut bolt, a measurement) gets typed in
here at whatever price was agreed. It rings up and prints
like any other line but never touches stock. -->
<button class="custom-item-btn" data-od-id="add-custom-item" onclick="openCustomItem()">
+ Add a custom item
</button>
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
<!-- The discount trigger rides inside the Subtotal row so the footer
costs no extra height. Tapping it expands the R / % pills, number
input and Apply button below; the discounted amount is then shown
in red on its own totals line, and the server re-derives the
resolved rand value at commit time. -->
<div class="totals">
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<span>Subtotal (ex VAT)
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
<button class="discount-trigger" id="discountTrigger" data-od-id="discount-trigger" onclick="toggleDiscount()">
<span id="discountTriggerLabel">Add discount</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>
</button>
</span>
<b id="subTotal">R 0.00</b>
</div>
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<div class="totals"><span id="vatLabel">VAT 15%</span><b id="vatTotal">R 0.00</b></div>
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
<div class="discount-body" id="discountBody" hidden>
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<div class="discount-pills" role="group" aria-label="Discount tier">
<button class="discount-pill" id="discTier20" data-od-id="discount-20" onclick="setDiscountTier(20)">20% off</button>
<button class="discount-pill" id="discTier40" data-od-id="discount-40" onclick="setDiscountTier(40)">40% off</button>
</div>
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<p class="discount-hint" id="discHint">Tap a tier to apply it. Tap it again to clear.</p>
</div>
<div class="totals discount-line" id="discountLine" hidden>
<span>Discount</span><b id="discountLineAmount">− R 0.00</b>
</div>
<div class="totals grand"><span>Total (incl. VAT)</span><b id="grandTotal">R 0.00</b></div>
<button class="final-cta" data-od-id="cart-checkout" onclick="closeDrawer();openPayment()">
Take payment
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
<!-- Payment drawer -->
<div class="drawer" id="payDrawer">
<div class="sheet">
<div class="sheet-head">
<h2>Take payment</h2>
<button class="x" onclick="closePayment()" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
<div class="sheet-body">
<div class="totals" style="padding:10px 20px 0"><span>Subtotal</span><b id="paySub">R 0.00</b></div>
<div class="totals" style="padding:0 20px"><span>VAT 15%</span><b id="payVat">R 0.00</b></div>
<div class="totals grand" style="padding:14px 20px"><span>Total due</span><b id="payTotal">R 0.00</b></div>
<div class="pay-grid" id="payGrid">
<button class="pay-tile" data-od-id="pay-cash" data-meth="cash" onclick="pickMethod('cash')">
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="12" rx="2"/><circle cx="12" cy="12" r="2.5"/></svg></div><div class="lab">Cash</div><div class="sub">Calculate change</div></div>
</button>
<button class="pay-tile" data-od-id="pay-card" data-meth="card" onclick="pickMethod('card')">
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="12" rx="2"/><path d="M3 10h18"/></svg></div><div class="lab">Card</div><div class="sub">No change</div></div>
</button>
<button class="pay-tile" data-od-id="pay-eft" data-meth="eft" onclick="pickMethod('eft')">
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 7h16M4 12h16M4 17h10"/></svg></div><div class="lab">EFT</div><div class="sub">Instant transfer</div></div>
</button>
<button class="pay-tile" data-od-id="pay-account" data-meth="account" onclick="pickMethod('account')">
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg></div><div class="lab">Account</div><div class="sub">Print delivery note</div></div>
</button>
</div>
<div class="cash-block" id="cashBlock">
<div class="cash-row">
<div class="field"><label>Total due</label><input id="dueIn" type="text" readonly tabindex="-1"></div>
<div class="field"><label>Cash given</label><input id="givenIn" type="text" inputmode="decimal" placeholder="0.00"></div>
</div>
<div class="quick" id="quick"></div>
<div class="change-out" id="changeOut">
<div class="lbl">Change to give</div>
<div class="amt" id="changeAmt">R 0.00</div>
</div>
</div>
<div class="cust-block" id="custBlock">
<div class="cust-picked" id="custPicked"><b></b><button onclick="clearCust()">Remove</button></div>
<div class="search-row">
<input id="custQ" placeholder="Search customer" autocomplete="off">
</div>
<div class="cust-results" id="custResults"></div>
</div>
</div>
<div style="padding:0 20px 18px">
<button class="final-cta" id="confirmBtn" data-od-id="confirm-pay" disabled onclick="confirmSale()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12l5 5 9-11"/></svg>
Complete sale
</button>
</div>
</div>
</div>
<!-- Edit line modal (long-press a cart row to open) -->
<div class="edit-backdrop" id="editBackdrop" onclick="closeEditLine()"></div>
<div class="edit-sheet" id="editSheet" role="dialog" aria-labelledby="editTitle" aria-modal="true">
<h3 id="editTitle">Edit line <small id="editSku"></small></h3>
<div class="edit-row">
<label for="editQty">Quantity</label>
<input id="editQty" type="text" inputmode="decimal" autocomplete="off" data-od-id="edit-qty">
</div>
fix(pos): make cart quantity editable, and fix the sheet that hid it Ringing up 800 of something means typing 800, and the only route to the quantity field was a 600ms long-press with no affordance anywhere on screen. The quantity itself — the thing a cashier reaches for — was a plain <div class="v">, so it rendered inert. - the quantity is now a real button that opens the Edit line sheet, with an aria-label and a 44px touch target (it was 30px, too narrow to even show "800") - holding +/- no longer pops the sheet open mid-tap: the pointerdown handler bails for .qty/.rm before starting the timer - the sheet is quantity-only. The Unit price field is gone on request; a mis-keyed price is expensive and easy to miss. The per-line override it fed is removed with it — pos-service.ts only ever read unit_price, the "price edited" label and the sale record's `edited` flag were unreachable, and nothing else could set an override, including a cart reload (persistCart saves only {id, qty, p}) The discount trigger moved out of its own 54px footer row to ride inside the Subtotal row, so it costs no vertical height at all while staying visible — a discount control that appears from nowhere is one nobody finds under pressure. The applied-discount red totals row remains the always-on indicator. Fixes a pre-existing bug found while testing: long-pressing a cart row showed a grey screen with no dialog. #editSheet is a SIBLING of #editBackdrop, but the CSS revealed it with `.edit-backdrop.show .edit-sheet` — a descendant selector that can never match a sibling — so the sheet sat at opacity:0 while the backdrop went opaque. Nothing threw and the build was green, which is why it survived; it is also why manual quantity entry looked missing. The sheet now has its own z-index above the backdrop and its own .show rule. They stay siblings on purpose: nesting the sheet inside the backdrop would make every tap on Save bubble to the backdrop's onclick and close the dialog. Verified in jsdom against the real markup: old CSS reproduces the grey screen (backdrop opacity 1, sheet opacity 0, not clickable), the fix clears it (sheet opacity 1, z-index 71 > 70, position fixed). 125 POS tests pass, including 23 new ones asserting the tap target exists, the long-press guard fires before the timer, the sheet stacks above its backdrop, and the popup stays quantity-only. sw.js v30 -> v33: pos-app.js and pos.html are both served cache-first out of the shell, so without the bump tills keep the old ones.
2026-10-07 10:54:47 +00:00
<p class="discount-hint" id="editHint">Type a quantity like 800, then tap Save.</p>
<div class="edit-actions">
<button class="danger" data-od-id="edit-remove" onclick="editRemove()">Remove</button>
<button class="secondary" data-od-id="edit-cancel" onclick="closeEditLine()">Cancel</button>
<button class="primary" data-od-id="edit-save" onclick="editSave()">Save</button>
</div>
</div>
feat(pos): ex-VAT totals, ad-hoc custom lines, fixed discount tiers Three changes to the POS sale pipeline, all in one commit because pos-service.ts / types.ts / pos-app.js are each touched by all three and splitting them would only produce intermediate broken states. VAT is now charged on top, not backed out Catalogue prices are ex-VAT: `selling_price` is derived as `cost_price x markup` from the supplier's ex-VAT cost, with no VAT uplift anywhere (verified — median selling/cost across the catalogue is exactly 1.8). The till nevertheless treated them as VAT-inclusive and recovered the net with `total / 1.15`, so every sale was recorded with an understated net and an understated VAT line. An R100 ex-VAT line now rings up as R115. The rate comes from the till's letterhead, read server-side rather than accepted off the wire, and is stored per sale so a later letterhead rate change cannot rewrite what was charged. All 3,623 catalogue prices are untouched. Ad-hoc "custom" lines A cashier can now ring up something that was never a catalogue item — "bolt cutting" at R20 agreed at the counter — via a new "+ Add a custom item" composer. The line prints on the slip and lands in pos_sale_items, but carries no stock_item relation and takes no stock movement: no ledger row, no current_qty decrement, no catalogue entry. pos_sale_items.stock_item is no longer required at the schema level and an explicit `is_custom` flag marks the line, rather than inferring it from a blank relation — so "how much did we make on bolt cutting this month" is a plain query, and a future stock-less line type cannot be confused with a corrupt one. Custom lines are refused on account sales: those raise a delivery note the customer signs for and feed their statement, so an ad-hoc charge must not slip onto one. Enforced server-side, not just in the UI. Discounts are now 20% or 40%, nothing else The R-off / %-off pills and the free-typed number box are gone. The server treats DISCOUNT_PERCENT_OPTIONS as an allowlist and refuses anything outside it rather than coercing it, so a stale till cannot invent a discount. The discount lands on the ex-VAT subtotal, making "20% off" a true 20% off both the net and the VAT on it. Two defects found while doing this pos_sales was missing six fields that checkoutSale has been writing all along (gross_subtotal, gross_vat, gross_total, discount_amount, discount_kind, discount_input). PocketBase accepts unknown fields without error and silently drops them, so all 192 historical sales lack them and a discounted sale reprinted as though no discount had been applied. Added, plus vat_percent. History also passed gross_total (VAT-inclusive) where the receipt derives the gross VAT from an ex-VAT base — a reprint double-counted VAT. Corrected. Verification: 3860 tests pass (14 new); typecheck, lint and production build clean; live PocketBase probe confirms a pos_sale_items row with stock_item="" and is_custom=true round-trips, with the probe removed afterwards.
2026-10-12 20:54:09 +00:00
<!-- Custom (ad-hoc) item composer -->
<div class="edit-backdrop" id="customBackdrop" onclick="closeCustomItem()"></div>
<div class="edit-sheet" id="customSheet" role="dialog" aria-labelledby="customTitle" aria-modal="true">
<h3 id="customTitle">Custom item</h3>
<p class="discount-hint">Anything that isn't stock — e.g. "Bolt cutting". It rings up at the price you type, and doesn't come out of stock.</p>
<div class="edit-row">
<label for="customDesc">Description</label>
<input id="customDesc" type="text" autocomplete="off" placeholder="Bolt cutting" data-od-id="custom-desc">
</div>
<div class="edit-row">
<label for="customPrice">Price (ex VAT)</label>
<input id="customPrice" type="text" inputmode="decimal" autocomplete="off" placeholder="0.00" data-od-id="custom-price">
</div>
<div class="edit-actions">
<button class="secondary" data-od-id="custom-cancel" onclick="closeCustomItem()">Cancel</button>
<button class="primary" data-od-id="custom-save" onclick="saveCustomItem()">Add to cart</button>
</div>
</div>
<script src="pos-config.js"></script>
<script src="theme.js"></script>
<script src="letterhead-config.js"></script>
feat(pos): tiered catalog search — word order, plurals, glued SKUs The till filtered the catalog with one case-insensitive includes() over sku + description, so word order decided whether you got anything: "nuts m20" returned 0 hits while "m20 nuts" returned 1. The filter also checked sku_normalised, brand and search_text — fields that exist on price_list but not on a stock_items row — so three of its five clauses could never fire, and name was never searched at all. Search moves into public/pos/pos-search.js, unit-tested from src/lib/pos-search.test.ts. Every tier is a strict superset of the old filter, so nothing that used to match stops matching. exact whole query as a run squish punctuation/spacing removed, so M20NUT finds M20 Hex Nut tokens every word present in any order plural "nuts" also tries "nut" glued "M20NUT" -> M20 AND NUT on letter/digit boundaries Measured on the live 3,591-row catalog: "bolts" 0 -> 884 (884 real bolt rows, and no row literally says "bolts"), "nuts m20" 0 -> 12, "nut hex" 0 -> 13, "washers flat" 0 -> 14, M20NUT 1 -> 12, zero lost matches. A plural widens to exactly the union with its singular, asserted per pair. Widening is safe because both step-2 features are shaped as supersets: plural forms are alternatives inside one clause, while split parts are separate required clauses. Making split parts alternatives instead reduces "M20NUT" to "has M20" — 242 rows. Edit-distance fuzzy matching was measured and rejected: "nuts" 15 -> 172, "m-20-n-u-t" -> 2,481. Also fixes three defects the live data caught, all reachable from a code review alone: - split kept the generic fragment, promoting "20" from "m20nut" to a required clause; a leading 1-2 letter run now merges into its digits - the split group dropped the other tokens, so "M20NUT WASHER" matched rows containing no washer; other tokens are now carried along - a punctuation-only query ("-", "---") tokenised to nothing and threw, which would have blanked the catalog at the till The index is built once per catalog load on both paths (IndexedDB and network), keeping derived strings off the items so the cache stays lean. 0.21-2.98 ms per keystroke, so no debounce is needed. filteredProducts() falls back to the old substring filter if PosSearch is missing, since a skewed service-worker cache would otherwise blank a shop's catalog. sw.js v28 -> v30: the new script must be in the app shell, and the second bump is required because pos-search.js is served cache-first and step 2 edited it in place.
2026-10-06 21:36:12 +00:00
<script src="pos-search.js"></script>
<script src="pos-app.js"></script>
<script>if("serviceWorker" in navigator){navigator.serviceWorker.register("sw.js").catch(function(){});}</script>
</body>
</html>