From 647b9fa369910ade8247fac632f4bbb5aa092179 Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Thu, 8 Oct 2026 21:15:58 +0200 Subject: [PATCH] Keyboard in the Gboard/Windows Urdu layout with a shift layer; physical keys by position Co-Authored-By: Claude Opus 5.5 --- web/src/layouts/Base.astro | 39 +++++++++++++++++++++++--------------- web/src/styles/global.css | 2 ++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index 03da5821..0dbec8dd 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -63,38 +63,47 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک try { localStorage.setItem('divan-font', root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh'); } catch (e) {} label(); }; - // Urdu keyboard: on-screen keys + phonetic typing (Latin keys -> Urdu) while open - const PHONETIC = { - q:'ق', w:'و', e:'ع', r:'ر', t:'ت', y:'ے', u:'ء', i:'ی', o:'ہ', p:'پ', - a:'ا', s:'س', d:'د', f:'ف', g:'گ', h:'ح', j:'ج', k:'ک', l:'ل', - z:'ز', x:'ش', c:'چ', v:'ط', b:'ب', n:'ن', m:'م', - A:'آ', T:'ٹ', D:'ڈ', R:'ڑ', N:'ں', Z:'ذ', S:'ص', G:'غ', H:'ھ', K:'خ', C:'ث', V:'ظ', X:'ژ', J:'ض', I:'ئ', O:'ۂ', Y:'ؤ', - ',':'،', '.':'۔', '?':'؟', ';':'؛', - }; - const ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm', 'ATDRNZSGH', 'KCVXJIOY', ',.?;']; + // Urdu keyboard, Gboard/Windows Urdu layout: on-screen keys, and physical keys by position while open + const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']]; + const SHIFT = [['ظ','ض','ذ','ڈ','ث','ّ','ۃ','ـ','چ','خ'], ['ژ','ز','ڑ','ں','ۂ','ء','آ','گ',''], ['','','ۓ','','ؤ','ئ','']]; + const KEYS = {}; // KeyQ -> [base, shift] + ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'].forEach((r, i) => [...r].forEach((c, j) => (KEYS['Key' + c] = [BASE[i][j], SHIFT[i][j]]))); + const PUNCT = { Comma: '،', Period: '۔', Semicolon: '؛', Slash: '؟' }; const q = document.getElementById('q'), kb = document.getElementById('kb'), kbBtn = document.getElementById('kb-toggle'); + let shift = false; + const key = (ch, cls = '') => ch ? `` : ''; + const render = () => { + const L = shift ? SHIFT : BASE; + kb.innerHTML = `
${L[0].map((c) => key(c)).join('')}
${L[1].map((c) => key(c)).join('')}
` + + `
${L[2].map((c) => key(c)).join('')}` + + '
' + + `
${key('،')}${key('۔')}${key('؟')}
`; + }; + render(); const insert = (text) => { const s = q.selectionStart ?? q.value.length, e = q.selectionEnd ?? q.value.length; q.setRangeText(text, s, e, 'end'); q.focus(); }; - kb.innerHTML = ROWS.map((r) => '
' + [...r].map((k) => - ``).join('') + '
').join('') + - '
'; kb.onmousedown = (ev) => ev.preventDefault(); // keep focus/caret in the search box kb.onclick = (ev) => { const b = ev.target.closest('button'); if (!b) return; + if (b.dataset.act === 'shift') { shift = !shift; render(); return; } if (b.dataset.act === 'bs') { const s = q.selectionStart, e = q.selectionEnd; q.setRangeText('', s === e ? Math.max(0, s - 1) : s, e, 'end'); q.focus(); - } else insert(b.dataset.ch); + return; + } + insert(b.dataset.ch); + if (shift) { shift = false; render(); } // one-shot shift, as on Gboard }; kbBtn.onclick = () => { kb.hidden = !kb.hidden; kbBtn.setAttribute('aria-pressed', String(!kb.hidden)); q.focus(); }; q.addEventListener('keydown', (ev) => { if (kb.hidden || ev.ctrlKey || ev.metaKey || ev.altKey) return; - const ch = PHONETIC[ev.key]; - if (ch) { ev.preventDefault(); insert(ch); } + const k = KEYS[ev.code], ch = k ? k[ev.shiftKey ? 1 : 0] : PUNCT[ev.code]; + if (k || ch) ev.preventDefault(); + if (ch) insert(ch); }); document.getElementById('theme-toggle').onclick = () => { diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 7e3b63f4..667d66b5 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -119,3 +119,5 @@ h1 + .muted { text-align: center; margin-top: 0; } } .kb button:hover { border-color: var(--brand); } .kb .wide { min-width: 12em; } +.kb .fn { min-width: 3.4em; background: var(--inner); } +.kb .fn.on { border-color: var(--brand); color: var(--brand); }