Skip to content

Instantly share code, notes, and snippets.

@mekanics
Last active June 1, 2026 09:21
Show Gist options
  • Select an option

  • Save mekanics/30201fc5c7fb57a9aacb1a4b755aadeb to your computer and use it in GitHub Desktop.

Select an option

Save mekanics/30201fc5c7fb57a9aacb1a4b755aadeb to your computer and use it in GitHub Desktop.
Fava - PR #2287
<!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