Signed-off-by: webbeef me@webbeef.org
+1004
-163
Diff
round #0
+158
ui/keyboard/emoji/data.js
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
};
+78
ui/keyboard/renderer.js
+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
me.webbeef.org
submitted
#0
1 commit
expand
collapse
keyboard: multiple layouts and emojis
Signed-off-by: webbeef <me@webbeef.org>
expand 0 comments
pull request successfully merged