Rewild Your Web

keyboard: multiple layouts and emojis #1

merged opened by me.webbeef.org targeting main from keyboard-emoji
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:f25qr3njhaoeolu46vylylju/sh.tangled.repo.pull/3mfxpbtr7ws22
+1004 -163
Diff #0
+158
ui/keyboard/emoji/data.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + export const categories = [ 4 + { 5 + id: "frequent", 6 + icon: "🕐", 7 + name: "Frequently Used", 8 + emoji: [ 9 + "😀", "😂", "🤣", "😊", "😍", "🥰", "😘", "😭", "😢", "😤", "😡", "🥺", "😱", "🤔", "🤗", 10 + "👍", "👎", "❤️", "🔥", "✨", "🎉", "👏", "🙏", "💪", "😎", "🥳", "🤩", "👀", "💀", "🫡", 11 + ], 12 + }, 13 + { 14 + id: "smileys", 15 + icon: "😀", 16 + name: "Smileys & Emotion", 17 + emoji: [ 18 + "😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "🫠", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", 19 + "😗", "☺️", "😚", "😙", "🥲", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🫢", "🫣", "🤫", "🤔", "🫡", 20 + "🤐", "🤨", "😐", "😑", "😶", "🫥", "😶‍🌫️", "😏", "😒", "🙄", "😬", "😮‍💨", "🤥", "🫨", "😌", "😔", "😪", "🤤", "😴", 21 + "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "🥵", "🥶", "🥴", "😵", "😵‍💫", "🤯", "🤠", "🥳", "🥸", "😎", "🤓", "🧐", 22 + "😕", "🫤", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳", "🥺", "🥹", "😦", "😧", "😨", "😰", "😥", 23 + "😢", "😭", "😱", "😖", "😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬", "😈", "👿", 24 + "💀", "☠️", "💩", "🤡", "👹", "👺", "👻", "👽", "👾", "🤖", "😺", "😸", "😹", "😻", "😼", "😽", 25 + "🙀", "😿", "😾", "🙈", "🙉", "🙊", "💌", "💘", "💝", "💖", "💗", "💓", "💔", "❤️‍🔥", "❤️‍🩹", 26 + "❤️", "🩷", "🧡", "💛", "💚", "💙", "💜", "🤎", "🖤", "🩶", "🤍", "💋", "💯", "💢", "💥", "💫", 27 + "💦", "💨", "🕳️", "💬", "👁️‍🗨️", "🗨️", "🗯️", "💭", "💤", 28 + ], 29 + }, 30 + { 31 + id: "people", 32 + icon: "👋", 33 + name: "People & Body", 34 + emoji: [ 35 + "👋", "🤚", "🖐️", "✋", "🖖", "🫱", "🫲", "🫳", "🫴", "🫷", "🫸", "🤞", "🫰", "🤟", "🤘", 36 + "🤙", "👈", "👉", "👆", "👇", "☝️", "🫵", "✌️", "👌", "🤌", "🤏", "✍️", "👍", "👎", "✊", 37 + "👊", "🤛", "🤜", "👏", "🙌", "🫶", "👐", "🤲", "🤝", "🙏", "💅", "🤳", "💪", "🦾", "🦿", 38 + "🦵", "🦶", "👂", "🦻", "👃", "🧠", "🫀", "🫁", "🦷", "🦴", "👀", "👁️", "👅", "👄", "🫦", 39 + "👶", "🧒", "👦", "👧", "🧑", "👱", "👨", "🧔", "👩", "🧓", "👴", "👵", "🙍", "🙎", "🙅", 40 + "🙆", "💁", "🙋", "🧏", "🙇", "🤦", "🤷", "👮", "🕵️", "💂", 41 + ], 42 + }, 43 + { 44 + id: "animals", 45 + icon: "🐱", 46 + name: "Animals & Nature", 47 + emoji: [ 48 + "🐵", "🐒", "🦍", "🦧", "🐶", "🐕", "🦮", "🐕‍🦺", "🐩", "🐺", "🦊", "🦝", "🐱", "🐈", "🐈‍⬛", 49 + "🦁", "🐯", "🐅", "🐆", "🐴", "🫎", "🫏", "🐎", "🦄", "🦓", "🦌", "🦬", "🐮", "🐂", "🐃", 50 + "🐄", "🐷", "🐖", "🐗", "🐽", "🐏", "🐑", "🐐", "🐪", "🐫", "🦙", "🦒", "🐘", "🦣", "🦏", 51 + "🦛", "🐭", "🐁", "🐀", "🐹", "🐰", "🐇", "🐿️", "🦫", "🦔", "🦇", "🐻", "🐻‍❄️", "🐨", "🐼", 52 + "🦥", "🦦", "🦨", "🦘", "🦡", "🐾", "🦃", "🐔", "🐓", "🐣", "🐤", "🐥", "🐦", "🐧", "🕊️", 53 + "🦅", "🦆", "🦢", "🦉", "🦤", "🪶", "🦩", "🦚", "🦜", "🪽", "🪿", "🐸", "🐊", "🐢", "🦎", 54 + "🐍", "🐲", "🐉", "🦕", "🦖", "🐳", "🐋", "🐬", "🦭", "🐟", "🐠", "🐡", "🦈", "🐙", "🐚", 55 + "🪸", "🪼", "🐌", "🦋", "🐛", "🐜", "🐝", "🪲", "🐞", "🦗", "🪳", "🕷️", "🕸️", "🦂", "🦟", 56 + "🪰", "🪱", "🦠", "💐", "🌸", "💮", "🪷", "🏵️", "🌹", "🥀", "🌺", "🌻", "🌼", "🌷", "🪻", 57 + "🌱", "🪴", "🌲", "🌳", "🌴", "🌵", "🌾", "🌿", "☘️", "🍀", "🍁", "🍂", "🍃", "🪹", "🪺", 58 + ], 59 + }, 60 + { 61 + id: "food", 62 + icon: "🍔", 63 + name: "Food & Drink", 64 + emoji: [ 65 + "🍇", "🍈", "🍉", "🍊", "🍋", "🍋‍🟩", "🍌", "🍍", "🥭", "🍎", "🍏", "🍐", "🍑", "🍒", "🍓", 66 + "🫐", "🥝", "🍅", "🫒", "🥥", "🥑", "🍆", "🥔", "🥕", "🌽", "🌶️", "🫑", "🥒", "🥬", "🥦", 67 + "🧄", "🧅", "🍄", "🥜", "🫘", "🌰", "🍞", "🥐", "🥖", "🫓", "🥨", "🥯", "🥞", "🧇", "🧀", 68 + "🍖", "🍗", "🥩", "🥓", "🍔", "🍟", "🍕", "🌭", "🥪", "🌮", "🌯", "🫔", "🥙", "🧆", "🥚", 69 + "🍳", "🥘", "🍲", "🫕", "🥣", "🥗", "🍿", "🧈", "🧂", "🥫", "🍱", "🍘", "🍙", "🍚", "🍛", 70 + "🍜", "🍝", "🍠", "🍢", "🍣", "🍤", "🍥", "🥮", "🍡", "🥟", "🥠", "🥡", "🦀", "🦞", "🦐", 71 + "🦑", "🦪", "🍦", "🍧", "🍨", "🍩", "🍪", "🎂", "🍰", "🧁", "🥧", "🍫", "🍬", "🍭", "🍮", 72 + "🍯", "🍼", "🥛", "☕", "🫖", "🍵", "🍶", "🍾", "🍷", "🍸", "🍹", "🍺", "🍻", "🥂", "🥃", 73 + "🫗", "🥤", "🧋", "🧃", "🧉", "🧊", "🥢", "🍽️", "🥄", "🔪", "🏺", "🫙", 74 + ], 75 + }, 76 + { 77 + id: "travel", 78 + icon: "✈️", 79 + name: "Travel & Places", 80 + emoji: [ 81 + "🌍", "🌎", "🌏", "🌐", "🗺️", "🧭", "🏔️", "⛰️", "🌋", "🗻", "🏕️", "🏖️", "🏜️", "🏝️", "🏞️", 82 + "🏟️", "🏛️", "🏗️", "🧱", "🪨", "🪵", "🛖", "🏘️", "🏚️", "🏠", "🏡", "🏢", "🏣", "🏤", "🏥", 83 + "🏦", "🏨", "🏩", "🏪", "🏫", "🏬", "🏭", "🏯", "🏰", "💒", "🗼", "🗽", "⛪", "🕌", "🛕", 84 + "🕍", "⛩️", "🕋", "⛲", "⛺", "🌁", "🌃", "🏙️", "🌄", "🌅", "🌆", "🌇", "🌉", "♨️", "🎠", 85 + "🛝", "🎡", "🎢", "💈", "🎪", "🚂", "🚃", "🚄", "🚅", "🚆", "🚇", "🚈", "🚉", "🚊", "🚝", 86 + "🚞", "🚋", "🚌", "🚍", "🚎", "🚐", "🚑", "🚒", "🚓", "🚔", "🚕", "🚖", "🚗", "🚘", "🚙", 87 + "🛻", "🚚", "🚛", "🚜", "🏎️", "🏍️", "🛵", "🦽", "🦼", "🛺", "🚲", "🛴", "🛹", "🛼", "🚏", 88 + "🛣️", "🛤️", "🛢️", "⛽", "🛞", "🚨", "🚥", "🚦", "🛑", "🚧", "⚓", "🛟", "⛵", "🛶", "🚤", 89 + "🛳️", "⛴️", "🛥️", "🚢", "✈️", "🛩️", "🛫", "🛬", "🪂", "💺", "🚁", "🚟", "🚠", "🚡", "🛰️", 90 + "🚀", "🛸", "🛎️", "🧳", "⌛", "⏳", "⌚", "⏰", "⏱️", "⏲️", "🕰️", "🕛", "🕧", "🕐", "🕜", 91 + "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡", "🕖", "🕢", "🕗", "🕣", "🕘", 92 + "🕤", "🕙", "🕥", "🕚", "🕦", "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌙", "🌚", 93 + "🌛", "🌜", "🌡️", "☀️", "🌝", "🌞", "🪐", "⭐", "🌟", "🌠", "🌌", "☁️", "⛅", "⛈️", "🌤️", 94 + "🌥️", "🌦️", "🌧️", "🌨️", "🌩️", "🌪️", "🌫️", "🌬️", "🌀", "🌈", "🌂", "☂️", "☔", "⛱️", "⚡", 95 + "❄️", "☃️", "⛄", "☄️", "🔥", "💧", "🌊", 96 + ], 97 + }, 98 + { 99 + id: "activities", 100 + icon: "⚽", 101 + name: "Activities", 102 + emoji: [ 103 + "🎃", "🎄", "🎆", "🎇", "🧨", "✨", "🎈", "🎉", "🎊", "🎋", "🎍", "🎎", "🎏", "🎐", "🎑", 104 + "🧧", "🎀", "🎁", "🎗️", "🎟️", "🎫", "🎖️", "🏆", "🏅", "🥇", "🥈", "🥉", "⚽", "⚾", "🥎", 105 + "🏀", "🏐", "🏈", "🏉", "🎾", "🥏", "🎳", "🏏", "🏑", "🏒", "🥍", "🏓", "🏸", "🥊", "🥋", 106 + "🥅", "⛳", "⛸️", "🎣", "🤿", "🎽", "🎿", "🛷", "🥌", "🎯", "🪀", "🪁", "🔫", "🎱", "🔮", 107 + "🪄", "🧿", "🪬", "🎮", "🕹️", "🎰", "🧩", "🧸", "🪅", "🪩", "🪆", "🎭", "🖼️", "🎨", "🧵", 108 + "🪡", "🧶", "🪢", "🎹", "🎷", "🎺", "🎸", "🪕", "🎻", "🪘", "🪇", "🪈", "🥁", "🪗", "🎬", 109 + ], 110 + }, 111 + { 112 + id: "objects", 113 + icon: "💡", 114 + name: "Objects", 115 + emoji: [ 116 + "👓", "🕶️", "🥽", "🥼", "🦺", "👔", "👕", "👖", "🧣", "🧤", "🧥", "🧦", "👗", "👘", "🥻", 117 + "🩱", "🩲", "🩳", "👙", "👚", "🪭", "👛", "👜", "👝", "🛍️", "🎒", "🩴", "👞", "👟", "🥾", 118 + "🥿", "👠", "👡", "🩰", "👢", "🪮", "👑", "👒", "🎩", "🎓", "🧢", "🪖", "⛑️", "📿", "💄", 119 + "💍", "💎", "🔇", "🔈", "🔉", "🔊", "📢", "📣", "📯", "🔔", "🔕", "🎶", "🎵", "🎙️", "🎚️", 120 + "🎛️", "📻", "📱", "📲", "☎️", "📞", "📟", "📠", "🔋", "🪫", "🔌", "💻", "🖥️", "🖨️", "⌨️", 121 + "🖱️", "🖲️", "💽", "💾", "💿", "📀", "🧮", "🎥", "🎞️", "📽️", "🎬", "📺", "📷", "📸", "📹", 122 + "📼", "🔍", "🔎", "🕯️", "💡", "🔦", "🏮", "🪔", "📔", "📕", "📖", "📗", "📘", "📙", "📚", 123 + "📓", "📒", "📃", "📜", "📄", "📰", "🗞️", "📑", "🔖", "🏷️", "💰", "🪙", "💴", "💵", "💶", 124 + "💷", "💸", "💳", "🧾", "💹", "✉️", "📧", "📨", "📩", "📤", "📥", "📦", "📫", "📪", "📬", 125 + "📭", "📮", "🗳️", "✏️", "✒️", "🖋️", "🖊️", "🖌️", "🖍️", "📝", "💼", "📁", "📂", "🗂️", "📅", 126 + "📆", "🗒️", "🗓️", "📇", "📈", "📉", "📊", "📋", "📌", "📍", "📎", "🖇️", "📏", "📐", "✂️", 127 + "🗃️", "🗄️", "🗑️", "🔒", "🔓", "🔏", "🔐", "🔑", "🗝️", "🔨", "🪓", "⛏️", "⚒️", "🛠️", "🗡️", 128 + "⚔️", "💣", "🪃", "🏹", "🛡️", "🪚", "🔧", "🪛", "🔩", "⚙️", "🗜️", "⚖️", "🦯", "🔗", "⛓️", 129 + "🪝", "🧰", "🧲", "🪜", "⚗️", "🧪", "🧫", "🧬", "🔬", "🔭", "📡", "💉", "🩸", "💊", "🩹", 130 + "🩼", "🩺", "🩻", "🚪", "🛗", "🪞", "🪟", "🛏️", "🛋️", "🪑", "🚽", "🪠", "🚿", "🛁", "🪤", 131 + "🪒", "🧴", "🧷", "🧹", "🧺", "🧻", "🪣", "🧼", "🫧", "🪥", "🧽", "🧯", "🛒", "🚬", "⚰️", 132 + "🪦", "⚱️", "🗿", "🪧", "🪪", 133 + ], 134 + }, 135 + { 136 + id: "symbols", 137 + icon: "♾️", 138 + name: "Symbols", 139 + emoji: [ 140 + "🏧", "🚮", "🚰", "♿", "🚹", "🚺", "🚻", "🚼", "🚾", "🛂", "🛃", "🛄", "🛅", "⚠️", "🚸", 141 + "⛔", "🚫", "🚳", "🚭", "🚯", "🚱", "🚷", "📵", "🔞", "☢️", "☣️", "⬆️", "↗️", "➡️", "↘️", 142 + "⬇️", "↙️", "⬅️", "↖️", "↕️", "↔️", "↩️", "↪️", "⤴️", "⤵️", "🔃", "🔄", "🔙", "🔚", "🔛", 143 + "🔜", "🔝", "🛐", "⚛️", "🕉️", "✡️", "☸️", "☯️", "✝️", "☦️", "☪️", "☮️", "🕎", "🔯", "🪯", 144 + "♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓", "⛎", "🔀", "🔁", 145 + "🔂", "▶️", "⏩", "⏭️", "⏯️", "◀️", "⏪", "⏮️", "🔼", "⏫", "🔽", "⏬", "⏸️", "⏹️", "⏺️", 146 + "⏏️", "🎦", "🔅", "🔆", "📶", "🛜", "📳", "📴", "♀️", "♂️", "⚧️", "✖️", "➕", "➖", "➗", 147 + "🟰", "♾️", "‼️", "⁉️", "❓", "❔", "❕", "❗", "〰️", "💱", "💲", "⚕️", "♻️", "⚜️", "🔱", 148 + "📛", "🔰", "⭕", "✅", "☑️", "✔️", "❌", "❎", "➰", "➿", "〽️", "✳️", "✴️", "❇️", "©️", 149 + "®️", "™️", "#️⃣", "*️⃣", "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", 150 + "🔠", "🔡", "🔢", "🔣", "🔤", "🅰️", "🆎", "🅱️", "🆑", "🆒", "🆓", "ℹ️", "🆔", "Ⓜ️", "🆕", 151 + "🆖", "🅾️", "🆗", "🅿️", "🆘", "🆙", "🆚", "🈁", "🈂️", "🈷️", "🈶", "🈯", "🉐", "🈹", "🈚", 152 + "🈲", "🉑", "🈸", "🈴", "🈳", "㊗️", "㊙️", "🈺", "🈵", "🔴", "🟠", "🟡", "🟢", "🔵", "🟣", 153 + "🟤", "⚫", "⚪", "🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "🟫", "⬛", "⬜", "◼️", "◻️", "◾", 154 + "◽", "▪️", "▫️", "🔶", "🔷", "🔸", "🔹", "🔺", "🔻", "💠", "🔘", "🔳", "🔲", "🏁", "🚩", 155 + "🎌", "🏴", "🏳️", "🏳️‍🌈", "🏳️‍⚧️", "🏴‍☠️", 156 + ], 157 + }, 158 + ];
+151
ui/keyboard/emoji/renderer.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + /** 4 + * Render the emoji keyboard into the container. 5 + * 6 + * @param {HTMLElement} container - The #keyboard element. 7 + * @param {Array} categories - Emoji categories from emoji_data.js. 8 + * @param {Object} callbacks - { onEmoji, onClose, onBackspace } 9 + * @returns {Function} cleanup - Call to disconnect observers. 10 + */ 11 + export function renderEmoji(container, categories, callbacks) { 12 + container.innerHTML = ""; 13 + 14 + const wrapper = document.createElement("div"); 15 + wrapper.className = "emoji-keyboard"; 16 + 17 + // --- Scrollable grid area --- 18 + const gridContainer = document.createElement("div"); 19 + gridContainer.className = "emoji-grid-container"; 20 + 21 + const categoryHeaders = []; 22 + 23 + for (const category of categories) { 24 + if (category.emoji.length === 0) continue; 25 + 26 + const header = document.createElement("div"); 27 + header.className = "emoji-category-header"; 28 + header.textContent = category.name; 29 + header.dataset.categoryId = category.id; 30 + gridContainer.appendChild(header); 31 + categoryHeaders.push({ id: category.id, element: header }); 32 + 33 + const grid = document.createElement("div"); 34 + grid.className = "emoji-grid"; 35 + 36 + for (const emoji of category.emoji) { 37 + const btn = document.createElement("button"); 38 + btn.className = "emoji-key"; 39 + btn.textContent = emoji; 40 + btn.dataset.emoji = emoji; 41 + grid.appendChild(btn); 42 + } 43 + 44 + gridContainer.appendChild(grid); 45 + } 46 + 47 + wrapper.appendChild(gridContainer); 48 + 49 + // --- Category bar --- 50 + const bar = document.createElement("div"); 51 + bar.className = "emoji-category-bar"; 52 + 53 + const tabs = []; 54 + for (const category of categories) { 55 + if (category.emoji.length === 0) continue; 56 + 57 + const tab = document.createElement("button"); 58 + tab.className = "emoji-tab"; 59 + tab.textContent = category.icon; 60 + tab.dataset.categoryId = category.id; 61 + tabs.push({ id: category.id, element: tab }); 62 + bar.appendChild(tab); 63 + } 64 + 65 + // ABC button 66 + const abcTab = document.createElement("button"); 67 + abcTab.className = "emoji-tab abc"; 68 + abcTab.textContent = "ABC"; 69 + bar.appendChild(abcTab); 70 + 71 + // Backspace button 72 + const bsTab = document.createElement("button"); 73 + bsTab.className = "emoji-tab backspace"; 74 + bsTab.textContent = "\u232b"; 75 + bar.appendChild(bsTab); 76 + 77 + wrapper.appendChild(bar); 78 + container.appendChild(wrapper); 79 + 80 + // --- Active category tracking via IntersectionObserver --- 81 + let activeTabId = categories[0]?.id; 82 + setActiveTab(activeTabId); 83 + 84 + const observer = new IntersectionObserver( 85 + (entries) => { 86 + for (const entry of entries) { 87 + if (entry.isIntersecting) { 88 + const id = entry.target.dataset.categoryId; 89 + if (id && id !== activeTabId) { 90 + activeTabId = id; 91 + setActiveTab(id); 92 + } 93 + } 94 + } 95 + }, 96 + { 97 + root: gridContainer, 98 + rootMargin: "0px 0px -80% 0px", 99 + threshold: 0, 100 + }, 101 + ); 102 + 103 + for (const { element } of categoryHeaders) { 104 + observer.observe(element); 105 + } 106 + 107 + function setActiveTab(id) { 108 + for (const tab of tabs) { 109 + tab.element.classList.toggle("active", tab.id === id); 110 + } 111 + } 112 + 113 + // --- Event handlers --- 114 + 115 + // Emoji clicks (delegated on grid container) 116 + gridContainer.addEventListener("click", (event) => { 117 + const btn = event.target.closest(".emoji-key"); 118 + if (!btn) { 119 + return; 120 + } 121 + callbacks.onEmoji(btn.dataset.emoji); 122 + }); 123 + 124 + // Category tab clicks 125 + bar.addEventListener("click", (event) => { 126 + const tab = event.target.closest(".emoji-tab"); 127 + if (!tab) { 128 + return; 129 + } 130 + 131 + if (tab === abcTab) { 132 + callbacks.onClose(); 133 + return; 134 + } 135 + 136 + if (tab === bsTab) { 137 + callbacks.onBackspace(); 138 + return; 139 + } 140 + 141 + const id = tab.dataset.categoryId; 142 + if (id) { 143 + const header = categoryHeaders.find((h) => h.id === id); 144 + if (header) { 145 + header.element.scrollIntoView({ behavior: "smooth", block: "start" }); 146 + } 147 + } 148 + }); 149 + 150 + return () => observer.disconnect(); 151 + }
+134 -12
ui/keyboard/index.css
··· 4 4 box-sizing: border-box; 5 5 } 6 6 7 + :root { 8 + --default-key-width: calc(100vw / 10) 9 + } 10 + 11 + html, body { 12 + height: 100%; 13 + } 14 + 7 15 body { 8 16 margin: 0; 9 17 padding: 4px; ··· 11 19 font-family: sans-serif; 12 20 display: flex; 13 21 justify-content: center; 14 - align-items: center; 15 - min-height: 100%; 22 + align-items: flex-end; 16 23 } 17 24 18 25 .keyboard { 19 26 display: flex; 20 27 flex-direction: column; 28 + justify-content: flex-end; 21 29 gap: 4px; 22 30 max-width: 600px; 23 31 width: 100%; 32 + height: 100%; 33 + min-height: 0; 24 34 } 25 35 26 36 .row { ··· 30 40 } 31 41 32 42 .key { 33 - width: 30px; 34 - height: 36px; 43 + width: var(--default-key-width); 44 + min-width: 0; 45 + height: 40px; 35 46 padding: 0 10px; 47 + overflow: hidden; 36 48 background: #404040; 37 49 border: none; 38 50 border-radius: 6px; ··· 47 59 user-select: none; 48 60 } 49 61 50 - .key:active { 62 + .key:active .key:hover { 51 63 background: #808080; 52 64 transform: translateY(1px); 53 65 } ··· 56 68 width: 54px; 57 69 } 58 70 59 - .key.extra-wide { 60 - width: 72px; 71 + .key.fill { 72 + flex: 1; 73 + min-width: 0; 74 + } 75 + 76 + .grid { 77 + display: grid; 78 + gap: 4px; 79 + justify-content: center; 80 + } 81 + 82 + .grid .key { 83 + width: auto; 84 + height: auto; 85 + min-height: 36px; 61 86 } 62 87 63 88 .key.space { ··· 65 90 width: 280px; 66 91 } 67 92 68 - .key.shift.active { 93 + .key.active { 69 94 background: #5a8cff; 70 95 } 71 96 72 - /* Number row keys are slightly smaller */ 73 - .row-numbers .key { 74 - min-width: 32px; 75 - font-size: 14px; 97 + @keyframes layout-label { 98 + 0% { color: transparent; } 99 + 15% { color: white; } 100 + 70% { color: white; } 101 + 100% { color: transparent; } 102 + } 103 + 104 + .key.layout-label { 105 + font-size: 13px; 106 + color: transparent; 107 + animation: layout-label 1.2s ease forwards; 76 108 } 77 109 78 110 /* Hidden state */ 79 111 .keyboard.hidden { 80 112 display: none; 81 113 } 114 + 115 + /* --- Emoji keyboard --- */ 116 + 117 + .emoji-keyboard { 118 + display: flex; 119 + flex-direction: column; 120 + max-width: 600px; 121 + width: 100%; 122 + flex: 1; 123 + min-height: 0; 124 + overflow: hidden; 125 + } 126 + 127 + .emoji-grid-container { 128 + flex: 1; 129 + min-height: 0; 130 + overflow: scroll; 131 + padding: 0 4px; 132 + scrollbar-width: thin; 133 + scrollbar-color: #555 transparent; 134 + } 135 + 136 + .emoji-category-header { 137 + padding: 4px 2px; 138 + font-size: 11px; 139 + color: #999; 140 + } 141 + 142 + .emoji-grid { 143 + display: grid; 144 + grid-template-columns: repeat(auto-fill, minmax(36px, 1fr)); 145 + gap: 2px; 146 + } 147 + 148 + .emoji-key { 149 + height: 36px; 150 + background: none; 151 + border: none; 152 + border-radius: 6px; 153 + font-size: 22px; 154 + cursor: pointer; 155 + display: flex; 156 + align-items: center; 157 + justify-content: center; 158 + padding: 0; 159 + line-height: 1; 160 + } 161 + 162 + .emoji-key:active { 163 + background: #404040; 164 + } 165 + 166 + .emoji-category-bar { 167 + display: flex; 168 + gap: 2px; 169 + padding: 4px; 170 + border-top: 1px solid #404040; 171 + flex-shrink: 0; 172 + } 173 + 174 + .emoji-tab { 175 + flex: 1; 176 + height: 30px; 177 + background: none; 178 + border: none; 179 + border-radius: 4px; 180 + font-size: 16px; 181 + color: white; 182 + cursor: pointer; 183 + display: flex; 184 + align-items: center; 185 + justify-content: center; 186 + padding: 0; 187 + opacity: 0.5; 188 + transition: opacity 0.15s ease; 189 + } 190 + 191 + .emoji-tab.active { 192 + opacity: 1; 193 + background: #404040; 194 + } 195 + 196 + .emoji-tab.abc { 197 + font-size: 11px; 198 + font-weight: 600; 199 + } 200 + 201 + .emoji-tab.backspace { 202 + font-size: 14px; 203 + }
+2 -57
ui/keyboard/index.html
··· 7 7 <link rel="stylesheet" href="index.css" /> 8 8 </head> 9 9 <body> 10 - <div class="keyboard" id="keyboard"> 11 - <div class="row row-numbers"> 12 - <button class="key" data-key="1">1</button> 13 - <button class="key" data-key="2">2</button> 14 - <button class="key" data-key="3">3</button> 15 - <button class="key" data-key="4">4</button> 16 - <button class="key" data-key="5">5</button> 17 - <button class="key" data-key="6">6</button> 18 - <button class="key" data-key="7">7</button> 19 - <button class="key" data-key="8">8</button> 20 - <button class="key" data-key="9">9</button> 21 - <button class="key" data-key="0">0</button> 22 - </div> 23 - <div class="row"> 24 - <button class="key" data-key="q">q</button> 25 - <button class="key" data-key="w">w</button> 26 - <button class="key" data-key="e">e</button> 27 - <button class="key" data-key="r">r</button> 28 - <button class="key" data-key="t">t</button> 29 - <button class="key" data-key="y">y</button> 30 - <button class="key" data-key="u">u</button> 31 - <button class="key" data-key="i">i</button> 32 - <button class="key" data-key="o">o</button> 33 - <button class="key" data-key="p">p</button> 34 - </div> 35 - <div class="row"> 36 - <button class="key" data-key="a">a</button> 37 - <button class="key" data-key="s">s</button> 38 - <button class="key" data-key="d">d</button> 39 - <button class="key" data-key="f">f</button> 40 - <button class="key" data-key="g">g</button> 41 - <button class="key" data-key="h">h</button> 42 - <button class="key" data-key="j">j</button> 43 - <button class="key" data-key="k">k</button> 44 - <button class="key" data-key="l">l</button> 45 - </div> 46 - <div class="row"> 47 - <button class="key wide shift" data-action="shift">⇧</button> 48 - <button class="key" data-key="z">z</button> 49 - <button class="key" data-key="x">x</button> 50 - <button class="key" data-key="c">c</button> 51 - <button class="key" data-key="v">v</button> 52 - <button class="key" data-key="b">b</button> 53 - <button class="key" data-key="n">n</button> 54 - <button class="key" data-key="m">m</button> 55 - <button class="key wide" data-action="backspace">⌫</button> 56 - </div> 57 - <div class="row"> 58 - <button class="key extra-wide" data-action="symbols">123</button> 59 - <button class="key" data-key=",">,</button> 60 - <button class="key space" data-key=" ">space</button> 61 - <button class="key" data-key=".">.</button> 62 - <button class="key extra-wide" data-action="return">↵</button> 63 - </div> 64 - </div> 65 - 66 - <script src="index.js"></script> 10 + <div class="keyboard" id="keyboard"></div> 11 + <script type="module" src="index.js"></script> 67 12 </body> 68 13 </html>
+220 -94
ui/keyboard/index.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-or-later 2 2 3 - let shiftActive = false; 4 - let inputType = "text"; 5 - let currentValue = ""; 6 - 7 - // Check if the keyboard API is available 8 - const keyboardAvailable = typeof navigator.keyboard !== "undefined"; 9 - if (keyboardAvailable) { 10 - console.log("[Keyboard] navigator.keyboard API is available"); 11 - } else { 12 - console.warn( 13 - "[Keyboard] navigator.keyboard API is NOT available - virtual keyboard will not function", 3 + import { renderVariant } from "./renderer.js"; 4 + import { renderEmoji } from "./emoji/renderer.js"; 5 + import { categories as emojiCategories } from "./emoji/data.js"; 6 + import en_US from "./layouts/en_US.js"; 7 + import fr_FR from "./layouts/fr_FR.js"; 8 + import es_ES from "./layouts/es_ES.js"; 9 + import de_DE from "./layouts/de_DE.js"; 10 + import numpad from "./layouts/numpad.js"; 11 + 12 + class KeyboardAPI { 13 + constructor() { 14 + this.available = typeof navigator.keyboard !== "undefined"; 15 + if (!this.available) { 16 + console.warn("[KeyboardAPI] navigator.keyboard API is not available"); 17 + } 18 + } 19 + 20 + sendCharacter(char) { 21 + if (!this.available) { 22 + return; 23 + } 24 + try { 25 + navigator.keyboard.sendCompositionEvent({ 26 + state: "end", 27 + data: char, 28 + }); 29 + } catch (e) { 30 + console.error("[KeyboardAPI] Failed to send character:", e); 31 + } 32 + } 33 + 34 + sendKeyEvent(key, code) { 35 + if (!this.available) { 36 + return; 37 + } 38 + try { 39 + navigator.keyboard.sendKeyboardEvent({ 40 + state: "down", 41 + key: key, 42 + code: code || key, 43 + }); 44 + navigator.keyboard.sendKeyboardEvent({ 45 + state: "up", 46 + key: key, 47 + code: code || key, 48 + }); 49 + } catch (e) { 50 + console.error("[KeyboardAPI] Failed to send key event:", e); 51 + } 52 + } 53 + } 54 + 55 + const keyboardAPI = new KeyboardAPI(); 56 + 57 + // --- Layout registry --- 58 + 59 + const TEXT_LAYOUTS = [ 60 + { id: "en-US", name: "English", layout: en_US }, 61 + { id: "fr-FR", name: "Fran\u00e7ais", layout: fr_FR }, 62 + { id: "es-ES", name: "Espa\u00f1ol", layout: es_ES }, 63 + { id: "de-DE", name: "Deutsch", layout: de_DE }, 64 + ]; 65 + 66 + const INPUT_TYPE_LAYOUTS = { 67 + number: { layout: numpad, defaultVariant: "default" }, 68 + }; 69 + 70 + let currentLayoutIndex = 0; 71 + let currentLayout = TEXT_LAYOUTS[0].layout; 72 + 73 + // --- Variant state --- 74 + 75 + const container = document.getElementById("keyboard"); 76 + let mode = "text"; // "text" | "emoji" 77 + let currentVariant = "lower"; 78 + let autoReturn = null; 79 + let emojiCleanup = null; 80 + 81 + function switchVariant(name) { 82 + currentVariant = name; 83 + autoReturn = null; 84 + render(); 85 + } 86 + 87 + function switchLayout() { 88 + currentLayoutIndex = (currentLayoutIndex + 1) % TEXT_LAYOUTS.length; 89 + currentLayout = TEXT_LAYOUTS[currentLayoutIndex].layout; 90 + currentVariant = "lower"; 91 + autoReturn = null; 92 + render(); 93 + showLayoutLabel(); 94 + } 95 + 96 + function showLayoutLabel() { 97 + const spaceKey = container.querySelector(".key.space"); 98 + if (!spaceKey) return; 99 + spaceKey.textContent = TEXT_LAYOUTS[currentLayoutIndex].name; 100 + spaceKey.classList.add("layout-label"); 101 + spaceKey.addEventListener( 102 + "animationend", 103 + () => { 104 + spaceKey.textContent = ""; 105 + spaceKey.classList.remove("layout-label"); 106 + }, 107 + { once: true }, 14 108 ); 15 109 } 16 110 17 - // Send a character to the active input via composition event 18 - function sendCharacter(char) { 19 - if (!keyboardAvailable) { 20 - return; 111 + function setInputType(inputType) { 112 + const special = INPUT_TYPE_LAYOUTS[inputType]; 113 + if (special) { 114 + currentLayout = special.layout; 115 + currentVariant = special.defaultVariant; 116 + } else { 117 + currentLayout = TEXT_LAYOUTS[currentLayoutIndex].layout; 118 + currentVariant = "lower"; 21 119 } 22 - try { 23 - navigator.keyboard.sendCompositionEvent({ 24 - state: "end", 25 - data: char, 26 - }); 27 - } catch (e) { 28 - console.error("[Keyboard] Failed to send character:", e); 120 + autoReturn = null; 121 + } 122 + 123 + function enterEmojiMode() { 124 + mode = "emoji"; 125 + emojiCleanup = renderEmoji(container, emojiCategories, { 126 + onEmoji: (emoji) => keyboardAPI.sendCharacter(emoji), 127 + onClose: () => exitEmojiMode(), 128 + onBackspace: () => keyboardAPI.sendKeyEvent("Backspace", "Backspace"), 129 + }); 130 + } 131 + 132 + function exitEmojiMode() { 133 + if (emojiCleanup) { 134 + emojiCleanup(); 135 + emojiCleanup = null; 29 136 } 137 + mode = "text"; 138 + render(); 139 + } 140 + 141 + function render() { 142 + renderVariant(container, currentLayout[currentVariant], currentVariant); 30 143 } 31 144 32 - // Send a keyboard event (keydown + keyup) for special keys 33 - function sendKeyEvent(key, code) { 34 - if (!keyboardAvailable) { 145 + // --- Event handling --- 146 + 147 + let pendingSwitch = null; 148 + 149 + container.addEventListener("click", (event) => { 150 + const button = event.target.closest(".key"); 151 + if (!button) { 35 152 return; 36 153 } 37 - try { 38 - navigator.keyboard.sendKeyboardEvent({ 39 - state: "down", 40 - key: key, 41 - code: code || key, 42 - }); 43 - navigator.keyboard.sendKeyboardEvent({ 44 - state: "up", 45 - key: key, 46 - code: code || key, 47 - }); 48 - } catch (e) { 49 - console.error("[Keyboard] Failed to send key event:", e); 154 + event.preventDefault(); 155 + 156 + const def = button._keyDef; 157 + 158 + // Variant-switch keys 159 + if (def.switchTo) { 160 + if (def.doubleSwitchTo) { 161 + // Defer the re-render so the button stays in the DOM for dblclick detection 162 + if (pendingSwitch) { 163 + clearTimeout(pendingSwitch); 164 + } 165 + pendingSwitch = setTimeout(() => { 166 + pendingSwitch = null; 167 + currentVariant = def.switchTo; 168 + autoReturn = def.autoReturn || null; 169 + render(); 170 + }, 300); 171 + } else { 172 + switchVariant(def.switchTo); 173 + } 174 + return; 175 + } 176 + 177 + // Action keys 178 + if (def.action) { 179 + if (def.action === "switchLayout") { 180 + switchLayout(); 181 + } else if (def.key) { 182 + keyboardAPI.sendKeyEvent(def.key, def.code); 183 + } 184 + return; 50 185 } 51 - } 52 186 53 - // Listen for messages from parent to get input context 187 + // Character keys 188 + if (def.value) { 189 + keyboardAPI.sendCharacter(def.value); 190 + if (autoReturn) { 191 + switchVariant(autoReturn); 192 + } 193 + } 194 + }); 195 + 196 + container.addEventListener("dblclick", (event) => { 197 + const button = event.target.closest(".key"); 198 + if (!button) { 199 + return; 200 + } 201 + const def = button._keyDef; 202 + 203 + if (def.doubleSwitchTo) { 204 + if (pendingSwitch) { 205 + clearTimeout(pendingSwitch); 206 + pendingSwitch = null; 207 + } 208 + switchVariant(def.doubleSwitchTo); 209 + } 210 + }); 211 + 212 + // Use a long press on the globe to switch to the emoji mode. 213 + container.addEventListener("contextmenu", (event) => { 214 + event.preventDefault(); 215 + 216 + const button = event.target.closest(".key"); 217 + if (!button || button._keyDef.action !== "switchLayout") { 218 + return; 219 + } 220 + enterEmojiMode(); 221 + }); 222 + 223 + // --- Communication with parent --- 224 + 54 225 window.addEventListener("message", (event) => { 55 226 if (event.data.type === "show") { 56 - inputType = event.data.inputType || "text"; 57 - currentValue = event.data.currentValue || ""; 227 + const inputType = event.data.inputType || "text"; 228 + const currentValue = event.data.currentValue || ""; 58 229 console.log("[Keyboard] Input context received:", { 59 230 inputType, 60 231 currentValue, 61 232 }); 62 - // TODO: Adapt keyboard layout based on inputType (e.g., number pad for "number") 63 - } 64 - }); 65 233 66 - // Update key labels based on shift state 67 - function updateKeyLabels() { 68 - document.querySelectorAll(".key[data-key]").forEach((key) => { 69 - const keyValue = key.dataset.key; 70 - if (keyValue.length === 1 && keyValue.match(/[a-z]/i)) { 71 - key.textContent = shiftActive 72 - ? keyValue.toUpperCase() 73 - : keyValue.toLowerCase(); 74 - } 75 - }); 234 + setInputType(inputType); 76 235 77 - // Update shift button visual state 78 - document.querySelectorAll(".shift").forEach((btn) => { 79 - btn.classList.toggle("active", shiftActive); 80 - }); 81 - } 82 - 83 - // Handle key clicks 84 - document.querySelectorAll(".key").forEach((key) => { 85 - key.addEventListener("click", (e) => { 86 - e.preventDefault(); 87 - 88 - const action = key.dataset.action; 89 - const keyValue = key.dataset.key; 90 - 91 - if (action === "shift") { 92 - shiftActive = !shiftActive; 93 - updateKeyLabels(); 94 - console.log("[Keyboard] Shift toggled:", shiftActive); 95 - } else if (action === "backspace") { 96 - console.log("[Keyboard] Backspace pressed"); 97 - sendKeyEvent("Backspace", "Backspace"); 98 - } else if (action === "return") { 99 - console.log("[Keyboard] Return pressed"); 100 - sendKeyEvent("Enter", "Enter"); 101 - } else if (action === "symbols") { 102 - console.log("[Keyboard] Symbols mode requested"); 103 - // TODO: Switch to symbols keyboard layout 104 - } else if (keyValue) { 105 - let charToSend = keyValue; 106 - if (shiftActive && keyValue.match(/[a-z]/i)) { 107 - charToSend = keyValue.toUpperCase(); 108 - shiftActive = false; // Auto-release shift after typing 109 - updateKeyLabels(); 110 - } 111 - console.log("[Keyboard] Key pressed:", charToSend); 112 - sendCharacter(charToSend); 236 + if (mode === "emoji") { 237 + exitEmojiMode(); 238 + } else { 239 + render(); 113 240 } 114 - }); 241 + } 115 242 }); 116 243 117 - // Initialize 118 - updateKeyLabels(); 244 + render();
+40
ui/keyboard/layouts/de_DE.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + SHIFT, 5 + SHIFT_FROM_UPPER, 6 + SHIFT_FROM_CAPS, 7 + BACKSPACE, 8 + RETURN, 9 + SPACE, 10 + GLOBE, 11 + TO_SYMBOLS, 12 + SYMBOLS1, 13 + SYMBOLS2, 14 + } from "./shared.js"; 15 + 16 + export default { 17 + lower: [ 18 + ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "\u00fc"], 19 + ["a", "s", "d", "f", "g", "h", "j", "k", "l", "\u00f6", "\u00e4"], 20 + [SHIFT, "y", "x", "c", "v", "b", "n", "m", "\u00df", BACKSPACE], 21 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 22 + ], 23 + 24 + upper: [ 25 + ["Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", "\u00dc"], 26 + ["A", "S", "D", "F", "G", "H", "J", "K", "L", "\u00d6", "\u00c4"], 27 + [SHIFT_FROM_UPPER, "Y", "X", "C", "V", "B", "N", "M", "\u1e9e", BACKSPACE], 28 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 29 + ], 30 + 31 + caps: [ 32 + ["Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", "\u00dc"], 33 + ["A", "S", "D", "F", "G", "H", "J", "K", "L", "\u00d6", "\u00c4"], 34 + [SHIFT_FROM_CAPS, "Y", "X", "C", "V", "B", "N", "M", "\u1e9e", BACKSPACE], 35 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 36 + ], 37 + 38 + symbols1: SYMBOLS1, 39 + symbols2: SYMBOLS2, 40 + };
+40
ui/keyboard/layouts/en_US.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + SHIFT, 5 + SHIFT_FROM_UPPER, 6 + SHIFT_FROM_CAPS, 7 + BACKSPACE, 8 + RETURN, 9 + SPACE, 10 + GLOBE, 11 + TO_SYMBOLS, 12 + SYMBOLS1, 13 + SYMBOLS2, 14 + } from "./shared.js"; 15 + 16 + export default { 17 + lower: [ 18 + ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], 19 + ["a", "s", "d", "f", "g", "h", "j", "k", "l"], 20 + [SHIFT, "z", "x", "c", "v", "b", "n", "m", BACKSPACE], 21 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 22 + ], 23 + 24 + upper: [ 25 + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], 26 + ["A", "S", "D", "F", "G", "H", "J", "K", "L"], 27 + [SHIFT_FROM_UPPER, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE], 28 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 29 + ], 30 + 31 + caps: [ 32 + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], 33 + ["A", "S", "D", "F", "G", "H", "J", "K", "L"], 34 + [SHIFT_FROM_CAPS, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE], 35 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 36 + ], 37 + 38 + symbols1: SYMBOLS1, 39 + symbols2: SYMBOLS2, 40 + };
+40
ui/keyboard/layouts/es_ES.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + SHIFT, 5 + SHIFT_FROM_UPPER, 6 + SHIFT_FROM_CAPS, 7 + BACKSPACE, 8 + RETURN, 9 + SPACE, 10 + GLOBE, 11 + TO_SYMBOLS, 12 + SYMBOLS1, 13 + SYMBOLS2, 14 + } from "./shared.js"; 15 + 16 + export default { 17 + lower: [ 18 + ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], 19 + ["a", "s", "d", "f", "g", "h", "j", "k", "l", "\u00f1"], 20 + [SHIFT, "z", "x", "c", "v", "b", "n", "m", BACKSPACE], 21 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 22 + ], 23 + 24 + upper: [ 25 + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], 26 + ["A", "S", "D", "F", "G", "H", "J", "K", "L", "\u00d1"], 27 + [SHIFT_FROM_UPPER, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE], 28 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 29 + ], 30 + 31 + caps: [ 32 + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], 33 + ["A", "S", "D", "F", "G", "H", "J", "K", "L", "\u00d1"], 34 + [SHIFT_FROM_CAPS, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE], 35 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 36 + ], 37 + 38 + symbols1: SYMBOLS1, 39 + symbols2: SYMBOLS2, 40 + };
+40
ui/keyboard/layouts/fr_FR.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + SHIFT, 5 + SHIFT_FROM_UPPER, 6 + SHIFT_FROM_CAPS, 7 + BACKSPACE, 8 + RETURN, 9 + SPACE, 10 + GLOBE, 11 + TO_SYMBOLS, 12 + SYMBOLS1, 13 + SYMBOLS2, 14 + } from "./shared.js"; 15 + 16 + export default { 17 + lower: [ 18 + ["a", "z", "e", "r", "t", "y", "u", "i", "o", "p"], 19 + ["q", "s", "d", "f", "g", "h", "j", "k", "l", "m"], 20 + [SHIFT, "w", "x", "c", "v", "b", "n", BACKSPACE], 21 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 22 + ], 23 + 24 + upper: [ 25 + ["A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P"], 26 + ["Q", "S", "D", "F", "G", "H", "J", "K", "L", "M"], 27 + [SHIFT_FROM_UPPER, "W", "X", "C", "V", "B", "N", BACKSPACE], 28 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 29 + ], 30 + 31 + caps: [ 32 + ["A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P"], 33 + ["Q", "S", "D", "F", "G", "H", "J", "K", "L", "M"], 34 + [SHIFT_FROM_CAPS, "W", "X", "C", "V", "B", "N", BACKSPACE], 35 + [TO_SYMBOLS, GLOBE, SPACE, ".", RETURN], 36 + ], 37 + 38 + symbols1: SYMBOLS1, 39 + symbols2: SYMBOLS2, 40 + };
+30
ui/keyboard/layouts/numpad.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + const BACKSPACE = { 4 + label: "\u232b", 5 + action: "backspace", 6 + key: "Backspace", 7 + code: "Backspace", 8 + gridColumn: "4", 9 + }; 10 + 11 + const RETURN = { 12 + label: "\u21b5", 13 + action: "return", 14 + key: "Enter", 15 + code: "Enter", 16 + gridColumn: "4", 17 + gridRow: "2 / 5", 18 + }; 19 + 20 + export default { 21 + default: { 22 + grid: 4, 23 + keys: [ 24 + "1", "2", "3", BACKSPACE, 25 + "4", "5", "6", RETURN, 26 + "7", "8", "9", 27 + "-", "0", ".", 28 + ], 29 + }, 30 + };
+71
ui/keyboard/layouts/shared.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + // Shared key definitions used across all keyboard layouts. 4 + 5 + export const SHIFT = { 6 + label: "\u21e7", 7 + switchTo: "upper", 8 + doubleSwitchTo: "caps", 9 + autoReturn: "lower", 10 + activeIn: ["upper", "caps"], 11 + size: "wide", 12 + }; 13 + 14 + export const SHIFT_FROM_UPPER = { 15 + label: "\u21e7", 16 + switchTo: "lower", 17 + doubleSwitchTo: "caps", 18 + activeIn: ["upper", "caps"], 19 + size: "wide", 20 + }; 21 + 22 + export const SHIFT_FROM_CAPS = { 23 + label: "\u21e7", 24 + switchTo: "lower", 25 + activeIn: ["upper", "caps"], 26 + size: "wide", 27 + }; 28 + 29 + export const BACKSPACE = { 30 + label: "\u232b", 31 + action: "backspace", 32 + key: "Backspace", 33 + code: "Backspace", 34 + size: "wide", 35 + }; 36 + 37 + export const RETURN = { 38 + label: "\u21b5", 39 + action: "return", 40 + key: "Enter", 41 + code: "Enter", 42 + size: "wide", 43 + }; 44 + 45 + export const SPACE = { label: "", value: " ", size: "space" }; 46 + export const TO_SYMBOLS = { label: "123", switchTo: "symbols1", size: "wide" }; 47 + export const TO_ALPHA = { label: "ABC", switchTo: "lower", size: "wide" }; 48 + export const GLOBE = { label: "\ud83c\udf10", action: "switchLayout" }; 49 + export const NUMBER_ROW = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]; 50 + 51 + export const SYMBOLS1 = [ 52 + NUMBER_ROW, 53 + ["@", "#", "$", "%", "&", "-", "+", "(", ")"], 54 + [ 55 + { label: "#+=", switchTo: "symbols2", size: "wide" }, 56 + "*", "\"", "'", ":", ";", "!", "?", 57 + BACKSPACE, 58 + ], 59 + [TO_ALPHA, GLOBE, SPACE, ".", RETURN], 60 + ]; 61 + 62 + export const SYMBOLS2 = [ 63 + ["/", "\\", "|", "~", "<", ">", "=", "[", "]"], 64 + ["`", "\u00b0", "\u00a3", "\u20ac", "\u00a5", "\u00b7", "^", "{", "}"], 65 + [ 66 + { label: "123", switchTo: "symbols1", size: "wide" }, 67 + "\u2022", "\u00a9", "\u00ae", "\u2122", "\u00bf", "\u00a1", "\u2026", 68 + BACKSPACE, 69 + ], 70 + [TO_ALPHA, GLOBE, SPACE, ".", RETURN], 71 + ];
+78
ui/keyboard/renderer.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + /** 4 + * Normalize a key definition. Strings become { label, value } objects. 5 + */ 6 + function normalizeKey(key) { 7 + if (typeof key === "string") { 8 + return { label: key, value: key }; 9 + } 10 + return key; 11 + } 12 + 13 + function renderKey(def, currentVariant) { 14 + const button = document.createElement("button"); 15 + button.className = "key"; 16 + button.textContent = def.label; 17 + button._keyDef = def; 18 + 19 + if (def.size) { 20 + button.classList.add(def.size); 21 + } 22 + 23 + if (def.activeIn && def.activeIn.includes(currentVariant)) { 24 + button.classList.add("active"); 25 + } 26 + 27 + if (def.gridRow) { 28 + button.style.gridRow = def.gridRow; 29 + } 30 + 31 + if (def.gridColumn) { 32 + button.style.gridColumn = def.gridColumn; 33 + } 34 + 35 + return button; 36 + } 37 + 38 + /** 39 + * Render a keyboard variant into the container element. 40 + * 41 + * A variant is either: 42 + * - An array of rows (row-based layout): [[key, key, ...], ...] 43 + * - A grid object: { grid: numColumns, keys: [key, key, ...] } 44 + * 45 + * @param {HTMLElement} container - The #keyboard element. 46 + * @param {Array|Object} variant - The variant definition. 47 + * @param {string} currentVariant - Name of the current variant (for active state on switch keys). 48 + */ 49 + export function renderVariant(container, variant, currentVariant) { 50 + container.innerHTML = ""; 51 + 52 + if (variant.grid) { 53 + // Grid layout 54 + const gridEl = document.createElement("div"); 55 + gridEl.className = "grid"; 56 + gridEl.style.gridTemplateColumns = `repeat(${variant.grid}, 1fr)`; 57 + 58 + for (const rawKey of variant.keys) { 59 + const def = normalizeKey(rawKey); 60 + gridEl.appendChild(renderKey(def, currentVariant)); 61 + } 62 + 63 + container.appendChild(gridEl); 64 + } else { 65 + // Row-based layout 66 + for (const row of variant) { 67 + const rowEl = document.createElement("div"); 68 + rowEl.className = "row"; 69 + 70 + for (const rawKey of row) { 71 + const def = normalizeKey(rawKey); 72 + rowEl.appendChild(renderKey(def, currentVariant)); 73 + } 74 + 75 + container.appendChild(rowEl); 76 + } 77 + } 78 + }

History

1 round 0 comments
sign up or login to add to the discussion
me.webbeef.org submitted #0
1 commit
expand
keyboard: multiple layouts and emojis
expand 0 comments
pull request successfully merged