Rewild Your Web

keyboard: multiple layouts and emojis

Signed-off-by: webbeef <me@webbeef.org>

authored by webbeef.tngl.sh and committed by tangled.org aa669139 6dd00e16

+1003 -162
+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>
+219 -93
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 = ""; 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"; 6 11 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", 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(); 30 139 } 31 140 32 - // Send a keyboard event (keydown + keyup) for special keys 33 - function sendKeyEvent(key, code) { 34 - if (!keyboardAvailable) { 141 + function render() { 142 + renderVariant(container, currentLayout[currentVariant], currentVariant); 143 + } 144 + 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; 50 175 } 51 - } 52 176 53 - // Listen for messages from parent to get input context 54 - window.addEventListener("message", (event) => { 55 - if (event.data.type === "show") { 56 - inputType = event.data.inputType || "text"; 57 - currentValue = event.data.currentValue || ""; 58 - console.log("[Keyboard] Input context received:", { 59 - inputType, 60 - currentValue, 61 - }); 62 - // TODO: Adapt keyboard layout based on inputType (e.g., number pad for "number") 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; 185 + } 186 + 187 + // Character keys 188 + if (def.value) { 189 + keyboardAPI.sendCharacter(def.value); 190 + if (autoReturn) { 191 + switchVariant(autoReturn); 192 + } 63 193 } 64 194 }); 65 195 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(); 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; 74 207 } 75 - }); 208 + switchVariant(def.doubleSwitchTo); 209 + } 210 + }); 76 211 77 - // Update shift button visual state 78 - document.querySelectorAll(".shift").forEach((btn) => { 79 - btn.classList.toggle("active", shiftActive); 80 - }); 81 - } 212 + // Use a long press on the globe to switch to the emoji mode. 213 + container.addEventListener("contextmenu", (event) => { 214 + event.preventDefault(); 82 215 83 - // Handle key clicks 84 - document.querySelectorAll(".key").forEach((key) => { 85 - key.addEventListener("click", (e) => { 86 - e.preventDefault(); 216 + const button = event.target.closest(".key"); 217 + if (!button || button._keyDef.action !== "switchLayout") { 218 + return; 219 + } 220 + enterEmojiMode(); 221 + }); 87 222 88 - const action = key.dataset.action; 89 - const keyValue = key.dataset.key; 223 + // --- Communication with parent --- 90 224 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); 225 + window.addEventListener("message", (event) => { 226 + if (event.data.type === "show") { 227 + const inputType = event.data.inputType || "text"; 228 + const currentValue = event.data.currentValue || ""; 229 + console.log("[Keyboard] Input context received:", { 230 + inputType, 231 + currentValue, 232 + }); 233 + 234 + setInputType(inputType); 235 + 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 + }