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.
518 lines
37 KiB
HTML
518 lines
37 KiB
HTML
<!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;
|
||
--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}
|
||
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}
|
||
.topbar .netpill{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:#717171;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)}
|
||
.search-box svg{width:18px;height:18px;color:#717171;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}
|
||
.cat-trigger .cat-chev{width:16px;height:16px;flex:0 0 auto;color:#717171;transition:transform .18s}
|
||
.cat-trigger[aria-expanded="true"] .cat-chev{transform:rotate(180deg)}
|
||
.cat-count{font-size:12px;color:#717171;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}
|
||
.cat-item .ct{font-size:12px;color:#717171;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}
|
||
.catalog .meta{padding:12px 0 6px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#717171;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}
|
||
.catalog .more small{font-size:11px;font-weight:500;color:#717171;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}
|
||
.prod .sub{font-size:11px;color:#717171;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}
|
||
.prod .price small{font-size:11px;color:#717171;font-weight:500;margin-left:2px}
|
||
|
||
/* ---- 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}
|
||
.cart-bar .count span{font-size:11px;color:#717171;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}
|
||
.line .nm small{display:block;font-size:11px;color:#717171;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)}
|
||
.qty .v{width:30px;text-align:center;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums}
|
||
.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}
|
||
.totals{display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:#717171;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}
|
||
|
||
/* ---- Discount block ---- */
|
||
.discount{padding:8px 0 4px}
|
||
.discount-trigger{display:flex;align-items:center;justify-content:space-between;width:100%;padding:10px 12px;border:1px dashed var(--muted);border-radius:10px;background:#fff;color:var(--fg);font-size:13px;font-weight:600;letter-spacing:-.005em;min-height:42px}
|
||
.discount-trigger:active{background:var(--surface)}
|
||
.discount-trigger svg{width:16px;height:16px;color:#717171;transition:transform .18s}
|
||
.discount-trigger[aria-expanded="true"] svg{transform:rotate(180deg)}
|
||
.discount-trigger.has-value{border-style:solid;border-color:var(--accent);color:var(--accent);background:#fff8fa}
|
||
.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}
|
||
.discount-pill{padding:7px 16px;border-radius:999px;font-size:13px;font-weight:600;letter-spacing:-.005em;color:#717171;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)}
|
||
.discount-suffix{font-size:13px;font-weight:600;color:#717171;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}
|
||
.discount-hint{font-size:11px;color:#717171;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}
|
||
.edit-sheet{position:absolute;left:50%;bottom:24px;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-backdrop.show .edit-sheet{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}
|
||
.edit-sheet h3 small{font-size:11px;font-weight:500;color:#717171;letter-spacing:.02em;margin-left:auto}
|
||
.edit-row{display:flex;flex-direction:column;gap:6px}
|
||
.edit-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#717171}
|
||
.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}
|
||
.pay-tile .sub{font-size:11px;color:#717171;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}
|
||
.cash-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#717171;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}
|
||
.change-out .lbl{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#717171}
|
||
.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)}
|
||
.cust-results small{color:#717171;font-size:11px;margin-left:auto;letter-spacing:.02em}
|
||
.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) ---- */
|
||
:root[data-theme="dark"]{
|
||
--bg:#0f0f0f; --surface:#1c1416; --fg:#f0f0f0; --muted:#2a2a2a; --border:#3a2429;
|
||
--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)}
|
||
:root[data-theme="dark"] .search-box{background:#181818;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)}
|
||
:root[data-theme="dark"] .search-box svg{color:#888}
|
||
:root[data-theme="dark"] .search-box input{color:var(--fg)}
|
||
:root[data-theme="dark"] .search-box input::placeholder{color:#666}
|
||
:root[data-theme="dark"] .chip{background:#181818;border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .chip.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
|
||
:root[data-theme="dark"] .cat-trigger{background:#181818;border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .cat-trigger .cat-chev{color:#888}
|
||
:root[data-theme="dark"] .cat-count{color:#888}
|
||
:root[data-theme="dark"] .cat-menu{background:#181818;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)}
|
||
:root[data-theme="dark"] .cat-item .ct{color:#888}
|
||
: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)}
|
||
:root[data-theme="dark"] .prod .sub{color:#888}
|
||
:root[data-theme="dark"] .prod .price{color:var(--fg)}
|
||
:root[data-theme="dark"] .prod .price small{color:#888}
|
||
:root[data-theme="dark"] .cart-bar{background:#181818;border-top-color:var(--muted);box-shadow:var(--shadow-up)}
|
||
:root[data-theme="dark"] .cart-bar .count span{color:#888}
|
||
:root[data-theme="dark"] .cart-bar .total{color:var(--fg)}
|
||
:root[data-theme="dark"] .drawer{background:rgba(0,0,0,.7)}
|
||
:root[data-theme="dark"] .sheet{background:#181818}
|
||
: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)}
|
||
:root[data-theme="dark"] .line .nm small{color:#888}
|
||
: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)}
|
||
:root[data-theme="dark"] .line .pr{color:var(--fg)}
|
||
:root[data-theme="dark"] .line .rm{color:#666}
|
||
:root[data-theme="dark"] .line .rm:active{background:#3a2429;color:var(--accent)}
|
||
:root[data-theme="dark"] .sheet-foot{background:#181818;border-top-color:var(--muted)}
|
||
:root[data-theme="dark"] .totals{color:#888}
|
||
:root[data-theme="dark"] .totals b{color:var(--fg)}
|
||
:root[data-theme="dark"] .totals.grand{border-top-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .discount-trigger{background:#181818;border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .discount-trigger svg{color:#888}
|
||
:root[data-theme="dark"] .discount-trigger.has-value{border-color:var(--accent);color:#ff6680;background:#2a1a1a}
|
||
:root[data-theme="dark"] .discount-trigger.has-value svg{color:#ff6680}
|
||
:root[data-theme="dark"] .discount-pills{background:#222;border-color:var(--muted)}
|
||
:root[data-theme="dark"] .discount-pill{color:#888}
|
||
:root[data-theme="dark"] .discount-pill.active{background:#181818;color:#ff6680;box-shadow:0 1px 2px rgba(0,0,0,.4)}
|
||
:root[data-theme="dark"] .discount-input-row input{background:#181818;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)}
|
||
:root[data-theme="dark"] .discount-suffix{color:#888}
|
||
:root[data-theme="dark"] .discount-hint{color:#888}
|
||
:root[data-theme="dark"] .totals.discount-line{color:#ff6680}
|
||
:root[data-theme="dark"] .totals.discount-line b{color:#ff6680}
|
||
:root[data-theme="dark"] .empty{color:#666}
|
||
:root[data-theme="dark"] .empty h3{color:var(--fg)}
|
||
:root[data-theme="dark"] .empty p{color:#888}
|
||
:root[data-theme="dark"] .netpill{background:transparent;color:#888;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)}
|
||
:root[data-theme="dark"] .pay-tile{background:#181818;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}
|
||
:root[data-theme="dark"] .cash-row input{background:#181818;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}
|
||
:root[data-theme="dark"] .cash-row label{color:#888}
|
||
:root[data-theme="dark"] .quick button{background:#181818;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)}
|
||
:root[data-theme="dark"] .change-out .lbl{color:#888}
|
||
:root[data-theme="dark"] .cust-block input{background:#181818;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)}
|
||
:root[data-theme="dark"] .cust-results{background:#181818;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)}
|
||
:root[data-theme="dark"] .cust-results small{color:#888}
|
||
:root[data-theme="dark"] .cust-picked{background:var(--surface);border-color:var(--border);color:var(--fg)}
|
||
:root[data-theme="dark"] .edit-sheet{background:#181818;box-shadow:0 20px 60px rgba(0,0,0,.6)}
|
||
:root[data-theme="dark"] .edit-sheet h3 small{color:#888}
|
||
:root[data-theme="dark"] .edit-row label{color:#888}
|
||
:root[data-theme="dark"] .edit-row input{background:#181818;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)}
|
||
:root[data-theme="dark"] .edit-actions .secondary{background:#181818;border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .edit-actions .secondary:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .edit-actions .danger{background:#181818;border-color:#5a2429;color:#ff6680}
|
||
:root[data-theme="dark"] .edit-actions .danger:active{background:#2a1a1a}
|
||
:root[data-theme="dark"] .line-holding{background:var(--surface)}
|
||
:root[data-theme="dark"] .final-cta:disabled{background:#2a2a2a;color:#666}
|
||
@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">
|
||
<div class="totals"><span>Subtotal</span><b id="subTotal">R 0.00</b></div>
|
||
<div class="totals"><span>VAT 15%</span><b id="vatTotal">R 0.00</b></div>
|
||
|
||
<!-- Discount row: collapsed by default ("Add discount"),
|
||
expanded when the operator taps. Two-pill toggle
|
||
(R / %), number input, Apply / Clear. The discounted
|
||
amount is shown in red on the total line below. -->
|
||
<div class="discount" id="discountBlock">
|
||
<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>
|
||
<div class="discount-body" id="discountBody" hidden>
|
||
<div class="discount-pills" role="tablist">
|
||
<button class="discount-pill" id="discKindAmount" data-kind="amount" onclick="setDiscountKind('amount')">R off</button>
|
||
<button class="discount-pill" id="discKindPercent" data-kind="percent" onclick="setDiscountKind('percent')">% off</button>
|
||
</div>
|
||
<div class="discount-input-row">
|
||
<input id="discInput" type="text" inputmode="decimal" placeholder="0" autocomplete="off" data-od-id="discount-input">
|
||
<span class="discount-suffix" id="discSuffix">R</span>
|
||
<button class="discount-apply" data-od-id="discount-apply" onclick="applyDiscount()">Apply</button>
|
||
</div>
|
||
<p class="discount-hint" id="discHint">Enter a value and tap Apply.</p>
|
||
</div>
|
||
</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>
|
||
<div class="edit-row">
|
||
<label for="editPrice">Unit price (R)</label>
|
||
<input id="editPrice" type="text" inputmode="decimal" autocomplete="off" data-od-id="edit-price">
|
||
</div>
|
||
<p class="discount-hint" id="editHint">Hold to edit again, or change a value and 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>
|
||
|
||
<script src="pos-config.js"></script>
|
||
<script src="theme.js"></script>
|
||
<script src="letterhead-config.js"></script>
|
||
<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>
|