multikb
multike
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="google-site-verification" content="YOykyV7jEUg5Nj2GeyhHUcewf9WW-Ruyke1v23KePrY" />
<title>Nejashi Keyboard | Full Mapping & Dual Layouts & Speech</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Imported bold weights for Noto Sans Arabic for maximum visibility -->
<link href="https://fonts.googleapis.com/css2?family=Abyssinica+SIL&family=Noto+Sans+Arabic:wght@400;600;700&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-app: #f0f2f5;
--bg-keyboard: #d1d4d9;
--key-bg: #ffffff;
--key-bg-func: #adb5bd;
--key-bg-active: #e9ecef;
--key-accent: #0d6efd;
--key-vowel: #d32f2f;
--text-main: #1a1a1a;
--text-func: #333;
--radius-key: 6px;
--shadow-key: 0 1px 2px rgba(0,0,0,0.3);
--font-ethiopic: 'Abyssinica SIL', serif;
--font-arabic: 'Noto Sans Arabic', sans-serif;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
touch-action: manipulation;
}
body {
font-family: 'Roboto', var(--font-ethiopic), sans-serif;
background-color: var(--bg-app);
margin: 0;
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- Output Container & Area --- */
.output-container {
position: relative;
flex: 1;
display: flex;
width: 100%;
background: #fff;
overflow: hidden; /* Contains the scrollable output safely */
}
#output {
width: 100%;
flex: 1;
padding: 30px 20px;
font-size: 32px;
text-align: center;
line-height: 1.5;
outline: none;
overflow-y: auto;
word-wrap: break-word;
user-select: text;
border: none;
font-family: var(--font-ethiopic);
}
#output:empty:before {
content: attr(placeholder);
color: #bbb;
}
/* --- Speech Recognition UI --- */
.mic-btn {
position: absolute;
bottom: 20px;
right: 20px;
width: 55px;
height: 55px;
border-radius: 50%;
background: var(--key-accent);
color: white;
border: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.25);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.mic-btn svg { width: 28px; height: 28px; fill: white; transition: transform 0.2s; }
.mic-btn.recording {
background: var(--key-vowel);
animation: pulse-recording 1.5s infinite;
}
.mic-btn.recording svg { transform: scale(1.1); }
@keyframes pulse-recording {
0% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.6); }
70% { box-shadow: 0 0 0 15px rgba(211, 47, 47, 0); }
100% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0); }
}
.speech-interim {
position: absolute;
bottom: 25px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 10px 20px;
border-radius: 25px;
font-size: 16px;
font-family: 'Roboto', sans-serif;
max-width: 70%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 20;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.speech-interim.hidden {
opacity: 0;
transform: translate(-50%, 10px);
}
/* --- Keyboard Area --- */
.keyboard-area {
background: var(--bg-keyboard);
padding: 8px 4px calc(8px + env(safe-area-inset-bottom, 12px)) 4px;
flex-shrink: 0;
border-top: 1px solid #bbb;
}
.keyboard-grid {
display: flex;
flex-direction: column;
gap: 5px;
max-width: 850px;
margin: 0 auto;
}
.row { display: flex; justify-content: center; gap: 5px; width: 100%; }
.key {
background: var(--key-bg);
color: var(--text-main);
border-radius: var(--radius-key);
height: 60px;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-key);
font-family: var(--font-ethiopic);
position: relative;
cursor: pointer;
}
.key:active, .key.pressed { background: var(--key-bg-active); transform: translateY(1px); box-shadow: none; }
/* Label Scaling */
.key-hint {
font-size: 16px;
line-height: 1;
color: #666;
margin-bottom: 2px;
}
.key-main {
font-size: 24px; /* Standard English Size */
line-height: 1;
font-weight: 700;
color: var(--text-main);
font-family: 'Roboto', sans-serif;
}
.key-main.ethio {
font-family: var(--font-ethiopic);
font-weight: 400;
}
/* Specific Arabic Highlight Styles */
.ar-hint {
font-family: var(--font-arabic);
font-size: 18px; /* Smaller than English but bold */
font-weight: 700;
line-height: 1;
margin-bottom: 2px;
color: #555;
}
.ar-main {
font-family: var(--font-arabic);
font-size: 34px;
font-weight: 700;
line-height: 1;
color: var(--text-main);
}
/* Vowel Colors */
.key.vowel .key-main,
.key.vowel .ar-hint,
.key.vowel .ar-main {
color: var(--key-vowel);
}
/* Functional Key Styling */
.key.func { background: var(--key-bg-func); color: var(--text-func); font-weight: 700; font-family: 'Roboto', sans-serif; }
.key.action { background: var(--key-accent); color: #fff; flex-grow: 1.4; font-weight: 600; font-size: 16px; }
.key.space { flex-grow: 3; font-size: 14px; color: #444; font-family: 'Roboto', sans-serif; font-weight: 700; }
.key.shift-on { background: #fff; color: var(--key-accent); border: 2px solid var(--key-accent); }
/* Layout Variations styling */
.key.variation-active {
background-color: var(--key-bg);
border: 2px solid var(--key-accent);
color: var(--key-accent);
font-weight: 700;
z-index: 2;
}
.key.variation-active .key-main {
color: var(--key-accent);
}
svg { width: 22px; height: 22px; fill: currentColor; }
.footer-credit {
text-align: center;
font-size: 10px;
color: #777;
margin-top: 8px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="output-container">
<div id="output" contenteditable="true" inputmode="none" placeholder="Type here..."></div>
<!-- Modern Floating Speech Overlay UI -->
<div id="speech-interim" class="speech-interim hidden">Listening...</div>
<button id="mic-btn" class="mic-btn" title="Start Speech Typing" onclick="toggleRecording()">
<svg viewBox="0 0 24 24">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
</button>
</div>
<div class="keyboard-area">
<div class="keyboard-grid" id="kb-grid"></div>
<div class="footer-credit">AHMEDHAJI SADIK © 2026</div>
</div>
<script>
// --- AMHARIC MAPPING (Layout 1 - Phonetic) ---
const AM_MAP_1 = {
'q':'ቀ', 'Q':'ቀ', 'w':'ወ', 'W':'ወ', 'r':'ረ', 'R':'ረ', 't':'ተ', 'T':'ጠ',
'y':'የ', 'Y':'የ', 'p':'ፐ', 'P':'ጰ',
's':'ሰ', 'S':'ሠ', 'd':'ደ', 'D':'ደ', 'f':'ፈ', 'F':'ፈ', 'g':'ገ', 'G':'ገ',
'h':'ሀ', 'H':'ሐ', 'j':'ጀ', 'J':'ጀ', 'k':'ከ', 'K':'ኸ',
'l':'ለ', 'L':'ጸ',
'z':'ዘ', 'Z':'ዠ', 'x':'አ', 'X':'ዐ', 'c':'ቸ', 'C':'ጨ',
'v':'ቨ', 'V':'ሸ',
'b':'በ', 'B':'ኀ',
'n':'ነ', 'N':'ኘ',
'm':'መ', 'M':'ፀ'
};
// --- AMHARIC MAPPING (Layout 2 - Visual Grids) ---
const AM_MAP_2 = {
'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':'ጰ', ',':'ጸ', '.':'ፈ',
'/':'ፀ', '=':'ፐ', 'backslash_alt': 'ዐ', 'shift_alt': 'ኀ'
};
// --- AMHARIC FIDEL DICTIONARY ---
const AM_FIDEL = {
'ሀ':['ሀ','ሁ','ሂ','ሃ','ሄ','ህ','ሆ','ኋ'],'ለ':['ለ','ሉ','ሊ','ላ','ሌ','ል','ሎ','ሏ'],'ሐ':['ሐ','ሑ','ሒ','ሓ','ሔ','ሕ','ሖ','ሗ'],'መ':['መ','ሙ','ሚ','ማ','ሜ','ም','ሞ','ሟ'],'ሠ':['ሠ','ሡ','ሢ','ሣ','ሤ','ሥ','ሦ','ሧ'],'ረ':['ረ','ሩ','ሪ','ራ','ሬ','ር','ሮ','ሯ'],'ሰ':['ሰ','ሱ','ሲ','ሳ','ሴ','ስ','ሶ','ሷ'],'ሸ':['ሸ','ሹ','ሺ','ሻ','ሼ','ሽ','ሾ','ሿ'],'ቀ':['ቀ','ቁ','ቂ','ቃ','ቄ','ቅ','ቆ','ቋ'],'በ':['በ','ቡ','ቢ','ባ','ቤ','ብ','ቦ','ቧ'],'ቨ':['ቨ','ቩ','ቪ','ቫ','ቬ','ቭ','ቮ','ቯ'],'ተ':['ተ','ቱ','ቲ','ታ','ቴ','ት','ቶ','ቷ'],'ቸ':['ቸ','ቹ','ቺ','ቻ','ቼ','ች','ቾ','ቿ'],'ኀ':['ኀ','ኁ','ኂ','ኃ','ኄ','ኅ','ኆ','ኋ'],'ነ':['ነ','ኑ','ኒ','ና','ኔ','ን','ኖ','ኗ'],'ኘ':['ኘ','ኙ','ኚ','ኛ','ኜ','ኝ','ኞ','ኟ'],'አ':['አ','ኡ','ኢ','ኣ','ኤ','እ','ኦ','ኧ'],'ከ':['ከ','ኩ','ኪ','ካ','ኬ','ክ','ኮ','ኳ'],'ኸ':['ኸ','ኹ','ኺ','ኻ','ኼ','ኽ','ኾ','ዃ'],'ወ':['ወ','ዉ','ዊ','ዋ','ዌ','ው','ዎ','ዋ'],'ዐ':['ዐ','ዑ','ዒ','ዓ','ዔ','ዕ','ዖ','ፇ'],'ዘ':['ዘ','ዙ','ዚ','ዛ','ዜ','ዝ','ዞ','ዟ'],'ዠ':['ዠ','ዡ','ዢ','ዣ','ዤ','ዥ','ዦ','ዧ'],'የ':['የ','ዩ','ዪ','ያ','ዬ','ይ','ዮ','ያ'],'ደ':['ደ','ዱ','ዲ','ዳ','ዴ','ድ','ዶ','ዷ'],'ጀ':['ጀ','ጁ','ጂ','ጃ','ጄ','ጅ','ጆ','ጇ'],'ገ':['ገ','ጉ','ጊ','ጋ','ጌ','ግ','ጎ','ጓ'],'ጠ':['ጠ','ጡ','ጢ','ጣ','ጤ','ጥ','ጦ','ጧ'],'ጨ':['ጨ','ጩ','ጪ','ጫ','ጬ','ጭ','ጮ','ጯ'],'ጰ':['ጰ','ጱ','ጲ','ጳ','ጴ','ጵ','ጶ','ጷ'],'ጸ':['ጸ','ጹ','ጺ','ጻ','ጼ','ጽ','ጾ','ጿ'],'ፀ':['ፀ','ፁ','ፂ','ፃ','ፄ','ፅ','ፆ','ጿ'],'ፈ':['ፈ','ፉ','ፊ','ፋ','ፌ','ፍ','ፎ','ፏ'],'ፐ':['ፐ','ፑ','ፒ','ፓ','ፔ','ፕ','ፖ','ፗ'],'።':['።']
};
// --- ARABIC MAPPING ---
const AR_MAP = {
'a': { d: 'ا', s: 'أ' }, 'y': { d: 'ي', s: 'ئ' }, 'h': { d: 'ح', s: 'ه' }, 'q': { d: 'ق' }, 'w': { d: 'و', s: 'ؤ' },
'e': { d: '\u064E', s: 'إ' }, 'r': { d: 'ر', s: 'ز' },
't': { d: 'ت', s: 'ة' }, 'u': { d: '\u064F' }, 'i': { d: '\u0650' },
'o': { d: '\u0652' }, 's': { d: 'س', s: 'ص' }, 'd': { d: 'د', s: 'ض' }, 'f': { d: 'ف' },
'g': { d: 'غ', s: 'ع' }, 'j': { d: 'ج' }, 'k': { d: 'ك', s: 'خ' }, 'l': { d: 'ل' }, 'z': { d: 'ذ', s: 'ظ' },
'x': { d: 'ء', s: 'ء' }, 'c': { d: 'ث', s: 'ش' }, 'v': { d: 'ظ' }, 'b': { d: 'ب' }, 'n': { d: 'ن' },
'm': { d: 'م' }, 'p': { d: 'ّ', s: 'ـ' }
};
const VOWELS = { 'e': 0, 'u': 1, 'i': 2, 'a': 3, 'y': 4, 'o': 6 };
const PURE_VOWELS = ['a', 'e', 'i', 'o', 'u'];
const PUNCT_DATA = {
'amharic': { left: '፣', right: '።' },
'english': { left: ',', right: '.' },
'arabic': { left: '،', right: '.' }
};
const DEFAULT_TOP_ROW = ['፡', '።', '፣', '፤', '፥', '፦', '፧', '፨', '?'];
let state = {
lang: 'amharic',
layoutAm: 1,
layoutAr: 1,
layoutEn: 1,
isShift: false,
isNum: false,
pendingConsonant: null,
lastVowel: null,
lastArKey: null,
lastArTime: 0,
currentTopRow: [...DEFAULT_TOP_ROW],
isVariationActive: false
};
const LAYOUT = {
alpha: [
['q','w','e','r','t','y','u','i','o','p'],
['a','s','d','f','g','h','j','k','l'],
['Shift','z','x','c','v','b','n','m','Back'],
['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']
],
amharic2: [
['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',',','.','Back'],
['!#1','Globe','Layout','/','=','Space','backslash_alt','shift_alt','Enter']
],
en2: [
['q','w','e','r','t','z','u','i','o','p'],
['a','s','d','f','g','h','j','k','l'],
['Shift','y','x','c','v','b','n','m','Back'],
['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']
],
ar2: [
['ض','ص','ث','ق','ف','غ','ع','ه','خ','ح'],
['ش','س','ي','ب','ل','ا','ت','ن','م','ك'],
['Shift','ظ','ط','ذ','د','ز','ر','و','ة','Back'],
['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']
],
num: [
['1','2','3','4','5','6','7','8','9','0'],
['@','#','$','%','&','-','+','(',')','/'],
['*','"',"'",':',';','!','?','_','=','Back'],
['ABC','Globe','punct_L','Space','punct_R','Enter']
]
};
const output = document.getElementById('output');
// --- SPEECH RECOGNITION LOGIC ---
const WebSpeechAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition = null;
let isRecording = false;
const micBtn = document.getElementById('mic-btn');
const speechInterim = document.getElementById('speech-interim');
if (WebSpeechAPI) {
recognition = new WebSpeechAPI();
recognition.continuous = true;
recognition.interimResults = true;
recognition.onstart = () => {
isRecording = true;
micBtn.classList.add('recording');
speechInterim.classList.remove('hidden');
speechInterim.textContent = "Listening...";
};
recognition.onresult = (event) => {
let interimTranscript = '';
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
} else {
interimTranscript += event.results[i][0].transcript;
}
}
if (interimTranscript) {
speechInterim.textContent = interimTranscript;
} else {
speechInterim.textContent = "Listening...";
}
if (finalTranscript) {
// Formatting pad with space
type(finalTranscript + ' ');
}
};
recognition.onerror = (event) => {
console.error("Speech recognition error", event.error);
speechInterim.textContent = "Error: " + event.error;
setTimeout(stopRecording, 1500);
};
recognition.onend = () => {
if (isRecording) {
try { recognition.start(); } catch(e) {} // Auto-restart continuous if not stopped manually
} else {
micBtn.classList.remove('recording');
speechInterim.classList.add('hidden');
}
};
} else {
micBtn.style.display = 'none'; // Hide UI if browser is unsupported
}
function toggleRecording() {
if (!recognition) {
alert("Speech typing is not supported in this browser. Please try Google Chrome or Safari.");
return;
}
if (isRecording) stopRecording();
else startRecording();
}
function startRecording() {
try {
// Set Dynamic Input Dictionary per mapped languages natively
if (state.lang === 'amharic') recognition.lang = 'am-ET';
else if (state.lang === 'arabic') recognition.lang = 'ar-SA';
else recognition.lang = 'en-US';
recognition.start();
} catch(e) { console.error(e); }
}
function stopRecording() {
isRecording = false;
micBtn.classList.remove('recording');
speechInterim.classList.add('hidden');
try { recognition.stop(); } catch(e) {}
}
// --- KEYBOARD LOGIC ---
function type(text) {
output.focus();
document.execCommand('insertText', false, text);
output.scrollTop = output.scrollHeight;
}
function resetState() {
state.pendingConsonant = null;
state.lastVowel = null;
state.lastArKey = null;
}
function resetTopRow() {
state.currentTopRow = [...DEFAULT_TOP_ROW];
state.isVariationActive = false;
}
function handleKey(k) {
if (state.lang === 'amharic' && state.layoutAm === 2 && state.currentTopRow.includes(k) && !state.isNum) {
if (state.isVariationActive) {
document.execCommand('delete');
type(k);
resetTopRow();
} else {
type(k);
}
render();
return;
}
if (k === 'Shift') { state.isShift = !state.isShift; render(); return; }
if (k === '!#1' || k === 'ABC') { state.isNum = !state.isNum; resetTopRow(); render(); return; }
if (k === 'Layout') {
if (state.lang === 'amharic') state.layoutAm = state.layoutAm === 1 ? 2 : 1;
else if (state.lang === 'arabic') state.layoutAr = state.layoutAr === 1 ? 2 : 1;
else if (state.lang === 'english') state.layoutEn = state.layoutEn === 1 ? 2 : 1;
resetState();
resetTopRow();
render();
return;
}
if (k === 'Globe') {
stopRecording(); // Stop recording when switching layout/languages
const langs = ['amharic','arabic','english'];
state.lang = langs[(langs.indexOf(state.lang) + 1) % 3];
output.dir = state.lang === 'arabic' ? 'rtl' : 'ltr';
resetState();
resetTopRow();
render();
return;
}
if (k === 'Back') {
document.execCommand('delete');
resetState();
resetTopRow();
render();
return;
}
if (k === 'Enter') { type('\n'); resetState(); resetTopRow(); render(); return; }
if (k === 'Space') { type(' '); resetState(); resetTopRow(); render(); return; }
if (k === 'punct_L') { type(PUNCT_DATA[state.lang].left); resetState(); return; }
if (k === 'punct_R') { type(PUNCT_DATA[state.lang].right); resetState(); return; }
if (state.isNum && k.length === 1) { type(k); return; }
let inputChar = state.isShift ? k.toUpperCase() : k.toLowerCase();
if (state.lang === 'amharic' && state.layoutAm === 2) {
if (state.isVariationActive) state.isVariationActive = false;
let mapKey = k.toLowerCase();
if (AM_MAP_2[mapKey] || AM_MAP_2[k]) {
let amChar = AM_MAP_2[mapKey] || AM_MAP_2[k];
if (amChar === '።') {
type(amChar);
resetTopRow();
} else if (AM_FIDEL[amChar]) {
type(AM_FIDEL[amChar][0]);
state.currentTopRow = AM_FIDEL[amChar];
state.isVariationActive = true;
} else {
type(amChar);
resetTopRow();
}
} else {
type(inputChar);
resetTopRow();
}
}
else if (state.lang === 'amharic' && state.layoutAm === 1) {
let keyLow = k.toLowerCase();
if (state.pendingConsonant && state.lastVowel === 'u' && keyLow === 'a') {
const base = AM_MAP_1[state.pendingConsonant];
if (AM_FIDEL[base] && AM_FIDEL[base][7]) {
document.execCommand('delete');
type(AM_FIDEL[base][7]);
}
resetState();
} else if (state.pendingConsonant && VOWELS.hasOwnProperty(keyLow)) {
document.execCommand('delete');
const base = AM_MAP_1[state.pendingConsonant];
type(AM_FIDEL[base][VOWELS[keyLow]]);
if (keyLow === 'u') { state.lastVowel = 'u'; } else { resetState(); }
} else if (PURE_VOWELS.includes(keyLow)) {
// Ignore pure vowels alone
} else if (AM_MAP_1[inputChar]) {
const base = AM_MAP_1[inputChar];
type(AM_FIDEL[base][5]);
state.pendingConsonant = inputChar;
state.lastVowel = null;
} else {
type(inputChar);
resetState();
}
}
else if (state.lang === 'arabic') {
if (state.layoutAr === 2) {
type(k);
state.lastArKey = null;
} else {
let keyLow = k.toLowerCase();
let now = Date.now();
let isDoubleTap = !state.isShift && (state.lastArKey === keyLow) && (now - state.lastArTime < 800);
const doubleTapMap = {
'y': 'ى', 'e': 'ً', 'u': 'ٌ', 'i': 'ٍ', 'o': 'ٰ'
};
if (isDoubleTap && doubleTapMap[keyLow]) {
document.execCommand('delete');
type(doubleTapMap[keyLow]);
state.lastArKey = keyLow + keyLow;
} else {
const entry = AR_MAP[keyLow];
type(entry ? (state.isShift ? (entry.s !== undefined ? entry.s : inputChar) : entry.d) : inputChar);
state.lastArKey = keyLow;
}
state.lastArTime = now;
}
}
else {
type(inputChar);
}
if (state.isShift) { state.isShift = false; }
render();
}
function render() {
const grid = document.getElementById('kb-grid');
grid.innerHTML = '';
let rows = [];
if (state.isNum) {
rows = LAYOUT.num;
} else if (state.lang === 'amharic' && state.layoutAm === 2) {
rows = [state.currentTopRow, ...LAYOUT.amharic2];
} else if (state.lang === 'arabic' && state.layoutAr === 2) {
rows = LAYOUT.ar2;
} else if (state.lang === 'english' && state.layoutEn === 2) {
rows = LAYOUT.en2;
} else {
rows = LAYOUT.alpha;
}
rows.forEach((row, rowIndex) => {
const rowEl = document.createElement('div');
rowEl.className = 'row';
row.forEach(key => {
const kEl = document.createElement('div');
kEl.className = 'key';
kEl.dataset.key = key.toLowerCase();
if (!state.isNum) {
let kLow = key.toLowerCase();
if (state.lang === 'amharic' && state.layoutAm === 1) {
if (['a', 'e', 'i', 'o', 'u', 'y'].includes(kLow)) kEl.classList.add('vowel');
} else if (state.lang === 'arabic' && state.layoutAr === 1) {
if (['a', 'e', 'i', 'o', 'u', 'p'].includes(kLow)) kEl.classList.add('vowel');
}
}
if (['Shift','Back','!#1','ABC','Globe','Layout'].includes(key)) kEl.classList.add('func');
if (key === 'Enter') kEl.classList.add('action');
if (key === 'Space') kEl.classList.add('space');
if (key === 'Shift' && state.isShift) kEl.classList.add('shift-on');
if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 2 && rowIndex === 0 && state.isVariationActive) {
kEl.classList.add('variation-active');
}
if (['Back', 'Shift', 'Globe'].includes(key)) {
if (key === 'Back') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-3 12.59L17.59 17 12 11.41 6.41 17 5 15.59 10.59 10 5 4.41 6.41 3 12 8.59 17.59 3 19 4.41 13.41 10 19 15.59z"/></svg>`;
else if (key === 'Shift') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 8.41L16.59 13 18 11.59l-6-6-6 6L7.41 13 12 8.41zM6 18h12v-2H6v2z"/></svg>`;
else if (key === 'Globe') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`;
} else if (key === 'Layout') {
let currentLay = state.lang === 'amharic' ? state.layoutAm : (state.lang === 'arabic' ? state.layoutAr : state.layoutEn);
kEl.innerHTML = `Lay ${currentLay}`;
} else if (key === 'Space') {
let currentLay = state.lang === 'amharic' ? state.layoutAm : (state.lang === 'arabic' ? state.layoutAr : state.layoutEn);
kEl.textContent = `${state.lang.toUpperCase()} (L${currentLay})`;
} else if (key === 'Enter' || key === '!#1' || key === 'ABC') {
kEl.textContent = key;
} else if (key === 'punct_L' || key === 'punct_R') {
const char = key === 'punct_L' ? PUNCT_DATA[state.lang].left : PUNCT_DATA[state.lang].right;
kEl.innerHTML = `<span class="key-main ethio">${char}</span>`;
} else if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 2) {
if (rowIndex === 0) {
kEl.innerHTML = `<span class="key-main ethio">${key}</span>`;
} else {
let amChar = '';
if (key === 'backslash_alt') amChar = AM_MAP_2['backslash_alt'];
else if (key === 'shift_alt') amChar = AM_MAP_2['shift_alt'];
else if (AM_MAP_2[key.toLowerCase()]) {
const base = AM_MAP_2[key.toLowerCase()];
amChar = AM_FIDEL[base] ? AM_FIDEL[base][0] : base;
} else {
amChar = key;
}
kEl.innerHTML = `<span class="key-main ethio">${amChar}</span>`;
}
} else if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 1) {
const engChar = state.isShift ? key.toUpperCase() : key.toLowerCase();
let amChar = '';
if (key.toLowerCase() === 'y') {
amChar = state.pendingConsonant ? 'ኤ' : 'ይ';
} else if (!PURE_VOWELS.includes(key.toLowerCase()) && AM_MAP_1[engChar]) {
const base = AM_MAP_1[engChar];
amChar = AM_FIDEL[base][5];
}
kEl.innerHTML = `<span class="key-hint">${amChar}</span><span class="key-main">${engChar}</span>`;
} else if (!state.isNum && state.lang === 'arabic') {
if (state.layoutAr === 1) {
const engChar = state.isShift ? key.toUpperCase() : key.toLowerCase();
let arChar = '';
if (AR_MAP[key.toLowerCase()]) {
const entry = AR_MAP[key.toLowerCase()];
const rawAr = state.isShift ? (entry.s !== undefined ? entry.s : entry.d) : entry.d;
arChar = /^[\u064B-\u065F]/.test(rawAr) ? '◌' + rawAr : rawAr;
}
kEl.innerHTML = `<span class="ar-hint">${arChar}</span><span class="key-main">${engChar}</span>`;
} else {
kEl.innerHTML = `<span class="ar-main">${key}</span>`;
}
} else {
const char = state.isShift ? key.toUpperCase() : key;
kEl.innerHTML = `<span class="key-main">${char}</span>`;
}
kEl.onpointerdown = (e) => { e.preventDefault(); handleKey(key); };
rowEl.appendChild(kEl);
});
grid.appendChild(rowEl);
});
}
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey || e.altKey) return;
let k = e.key;
if (k === 'Backspace') { e.preventDefault(); handleKey('Back'); return; }
if (k === 'Enter') { e.preventDefault(); handleKey('Enter'); return; }
if (k === ' ') { e.preventDefault(); handleKey('Space'); return; }
if (k === ',') { e.preventDefault(); handleKey('punct_L'); return; }
if (k === '.') { e.preventDefault(); handleKey('punct_R'); return; }
if (['Shift', 'CapsLock'].includes(k)) return;
state.isShift = e.shiftKey || e.getModifierState("CapsLock");
if (k.length === 1) {
e.preventDefault();
handleKey(k);
const btn = document.querySelector(`.key[data-key="${k.toLowerCase()}"]`);
if (btn) { btn.classList.add('pressed'); setTimeout(() => btn.classList.remove('pressed'), 100); }
}
});
render();
</script>
</body>
</html>