Last active
June 1, 2026 09:21
-
-
Save mekanics/30201fc5c7fb57a9aacb1a4b755aadeb to your computer and use it in GitHub Desktop.
Fava - PR #2287
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Fava balance input — mobile keyboard test</title> | |
| <style> | |
| :root { | |
| --bg: #f7f7f8; | |
| --card: #fff; | |
| --border: #d8d8de; | |
| --muted: #6b6b76; | |
| --accent: #4a7; | |
| --bad: #c0392b; | |
| --code-bg: #f0f0f3; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 1rem; | |
| font: 16px/1.5 system-ui, -apple-system, sans-serif; | |
| background: var(--bg); | |
| color: #1c1c22; | |
| } | |
| main { | |
| display: grid; | |
| gap: 1rem; | |
| max-width: 34rem; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| font-size: 1.25rem; | |
| margin: 0; | |
| } | |
| p.sub { | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| } | |
| .card { | |
| display: grid; | |
| gap: 0.75rem; | |
| padding: 1rem; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 0.6rem; | |
| } | |
| .card h2 { | |
| font-size: 1rem; | |
| margin: 0; | |
| } | |
| .row { | |
| display: grid; | |
| grid-template-columns: auto 1fr auto; | |
| gap: 0.5rem; | |
| align-items: center; | |
| } | |
| input { | |
| font: inherit; | |
| padding: 0.55rem 0.6rem; | |
| border: 1px solid var(--border); | |
| border-radius: 0.4rem; | |
| width: 100%; | |
| } | |
| input:invalid { | |
| border-color: var(--bad); | |
| } | |
| button.sign { | |
| font: inherit; | |
| width: 2.6rem; | |
| height: 2.6rem; | |
| border: 1px solid var(--border); | |
| border-radius: 0.4rem; | |
| background: #fff; | |
| cursor: pointer; | |
| } | |
| button.sign[aria-pressed="true"] { | |
| background: #ffecec; | |
| border-color: var(--bad); | |
| color: var(--bad); | |
| } | |
| .currency { | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| } | |
| dl { | |
| display: grid; | |
| grid-template-columns: max-content 1fr; | |
| gap: 0.25rem 0.75rem; | |
| margin: 0; | |
| font-size: 0.9rem; | |
| } | |
| dt { | |
| color: var(--muted); | |
| } | |
| dd { | |
| margin: 0; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace; | |
| } | |
| .pill { | |
| font-family: ui-monospace, monospace; | |
| font-size: 0.85rem; | |
| padding: 0.05rem 0.4rem; | |
| border-radius: 0.3rem; | |
| background: var(--code-bg); | |
| } | |
| .ok { | |
| color: var(--accent); | |
| font-weight: 600; | |
| } | |
| .err { | |
| color: var(--bad); | |
| font-weight: 600; | |
| } | |
| .grid-compare { | |
| display: grid; | |
| gap: 0.6rem; | |
| } | |
| .grid-compare label { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 0.2rem; | |
| font-size: 0.85rem; | |
| color: var(--muted); | |
| } | |
| code { | |
| font-family: ui-monospace, monospace; | |
| background: var(--code-bg); | |
| padding: 0 0.25rem; | |
| border-radius: 0.25rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header class="card"> | |
| <h1>Fava balance amount — mobile keyboard test</h1> | |
| <p class="sub"> | |
| Open this on a phone (try iOS in a comma-locale region, e.g. set the | |
| device language/region to German or French). Goal: a numeric keypad | |
| that supports a decimal separator <em>and</em> negative numbers on | |
| every platform. | |
| </p> | |
| </header> | |
| <section class="card"> | |
| <h2>inputmode="decimal" + normalize + ± toggle</h2> | |
| <div class="row"> | |
| <button | |
| type="button" | |
| class="sign" | |
| id="sign" | |
| aria-pressed="false" | |
| title="Toggle negative amount" | |
| > | |
| ± | |
| </button> | |
| <input | |
| id="amount" | |
| type="text" | |
| inputmode="decimal" | |
| pattern="-?[0-9.,]*" | |
| placeholder="Number" | |
| autocomplete="off" | |
| /> | |
| <span class="currency">USD</span> | |
| </div> | |
| <dl> | |
| <dt>Raw value</dt> | |
| <dd><span class="pill" id="raw">—</span></dd> | |
| <dt>Sent to backend</dt> | |
| <dd><span class="pill" id="normalized">—</span></dd> | |
| <dt>Decimal() valid?</dt> | |
| <dd id="valid">—</dd> | |
| </dl> | |
| <p class="sub"> | |
| The <code>±</code> button flips the sign (keypads have no minus key on | |
| iOS / Samsung). A typed locale comma is normalized to <code>.</code> | |
| before submit. | |
| </p> | |
| </section> | |
| <!-- KEYBOARD COMPARISON --> | |
| <section class="card"> | |
| <h2>Keyboard comparison (for screenshots)</h2> | |
| <p class="sub"> | |
| Tap each field and note which keyboard appears + whether | |
| <code>.</code>, <code>,</code> and <code>-</code> are available. | |
| </p> | |
| <div class="grid-compare"> | |
| <label> | |
| type="tel" (current main branch) | |
| <input type="tel" pattern="-?[0-9.,]*" placeholder="tel" /> | |
| </label> | |
| <label> | |
| type="number" | |
| <input type="number" step="any" placeholder="number" /> | |
| </label> | |
| <label> | |
| inputmode="numeric" | |
| <input type="text" inputmode="numeric" placeholder="numeric" /> | |
| </label> | |
| <label> | |
| inputmode="decimal" (this PR) | |
| <input type="text" inputmode="decimal" placeholder="decimal" /> | |
| </label> | |
| <label> | |
| inputmode="text" | |
| <input type="text" inputmode="text" placeholder="text" /> | |
| </label> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| // Same logic proposed for frontend/src/lib/numbers.ts | |
| function normalizeDecimal(value) { | |
| const last = Math.max(value.lastIndexOf("."), value.lastIndexOf(",")); | |
| if (last === -1) return value; | |
| const sign = value.startsWith("-") ? "-" : ""; | |
| const integer = value.slice(0, last).replace(/[-.,]/g, ""); | |
| const fraction = value.slice(last + 1).replace(/[.,]/g, ""); | |
| return `${sign}${integer}.${fraction}`; | |
| } | |
| // Mirrors what Python's Decimal(...) accepts. | |
| function isDecimalValid(value) { | |
| if (value === "") return false; | |
| return /^[+-]?(\d+\.?\d*|\.\d+)$/.test(value); | |
| } | |
| const input = document.getElementById("amount"); | |
| const sign = document.getElementById("sign"); | |
| const rawEl = document.getElementById("raw"); | |
| const normEl = document.getElementById("normalized"); | |
| const validEl = document.getElementById("valid"); | |
| function render() { | |
| const raw = input.value; | |
| const normalized = normalizeDecimal(raw); | |
| rawEl.textContent = raw === "" ? "(empty)" : raw; | |
| normEl.textContent = normalized === "" ? "(empty)" : normalized; | |
| sign.setAttribute("aria-pressed", String(normalized.startsWith("-"))); | |
| if (raw === "") { | |
| validEl.textContent = "—"; | |
| validEl.className = ""; | |
| } else if (isDecimalValid(normalized)) { | |
| validEl.textContent = "yes ✓ Decimal('" + normalized + "')"; | |
| validEl.className = "ok"; | |
| } else { | |
| validEl.textContent = "NO — backend would reject"; | |
| validEl.className = "err"; | |
| } | |
| } | |
| // Normalize on input so the stored value stays a valid number. | |
| input.addEventListener("input", () => { | |
| const normalized = normalizeDecimal(input.value); | |
| if (normalized !== input.value) { | |
| const atEnd = | |
| input.selectionStart === null || | |
| input.selectionStart === input.value.length; | |
| input.value = normalized; | |
| if (atEnd) { | |
| input.selectionStart = input.selectionEnd = input.value.length; | |
| } | |
| } | |
| render(); | |
| }); | |
| sign.addEventListener("click", () => { | |
| const n = input.value; | |
| input.value = n.startsWith("-") ? n.slice(1) : `-${n}`; | |
| input.focus(); | |
| render(); | |
| }); | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment