diff --git a/extension/sidepanel.html b/extension/sidepanel.html index 3160d80..120b3d1 100644 --- a/extension/sidepanel.html +++ b/extension/sidepanel.html @@ -25,6 +25,40 @@ #input-row { display: flex; gap: 6px; } #text { flex: 1; resize: none; height: 44px; } #send { cursor: pointer; background: #6b4fd6; border-color: #6b4fd6; padding: 0 14px; } + #emoji-btn { cursor: pointer; padding: 0 9px; font-size: 15px; line-height: 1; } + + #emoji-pad { + background: #1b1826; border: 1px solid #3a3548; border-radius: 6px; + padding: 6px; margin-bottom: 6px; + } + /* An ID selector outranks the UA's [hidden]{display:none}, so `hidden` alone + never closes this pad โ€” it has to be spelled out here. */ + #emoji-pad[hidden] { display: none; } + + #emoji-cats { + display: flex; gap: 2px; margin-bottom: 5px; + border-bottom: 1px solid #2c2836; padding-bottom: 5px; + } + #emoji-cats button { + flex: 1; border: 0; background: transparent; cursor: pointer; + font-size: 14px; line-height: 1; padding: 4px 0; border-radius: 4px; + } + #emoji-cats button:hover { background: #2f2a40; } + #emoji-cats button.on { background: #3a3155; box-shadow: inset 0 -2px 0 #a78bfa; } + + #emoji-lbl { + font-size: 10px; opacity: .45; margin-bottom: 4px; + text-transform: lowercase; letter-spacing: .04em; + } + #emoji-grid { + display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; + max-height: 168px; overflow-y: auto; + } + #emoji-grid button { + border: 0; background: transparent; padding: 4px 0; + font-size: 17px; line-height: 1.1; cursor: pointer; border-radius: 4px; + } + #emoji-grid button:hover { background: #2f2a40; } #status { font-size: 11px; opacity: .6; margin-bottom: 6px; } #ctx { font-size: 11px; color: #9d8fd0; margin-bottom: 6px; @@ -93,7 +127,13 @@
โ€”
connectingโ€ฆ
+
+
diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 58dbf9a..c7ebfd2 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -211,6 +211,88 @@ async function sendText() { } } +// โ”€โ”€ Emoji pad โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Category table lifted verbatim from celest-chat's 07-emoji.js โ€” same house, +// same taste, and the "house" row only means something in this room. Offline by +// construction: no library, no fetch, the panel's CSP would block a set anyway. +const EMOJI = [ + ["๐Ÿ","house", "๐Ÿ’œ ๐ŸŒ™ ๐Ÿ ๐Ÿ”ซ โœจ ๐Ÿ•ฏ๏ธ ๐Ÿ™ ๐ŸŒป ๐Ÿ–ค ๐Ÿฉต ๐ŸŒธ ๐Ÿ”ฅ โšก ๐ŸŽฏ ๐Ÿง  ๐Ÿช„ ๐Ÿ› ๏ธ ๐Ÿงป ๐Ÿ“ฌ ๐Ÿšช ๐Ÿ  โ™พ๏ธ ๐ŸŒŒ ๐Ÿซ– ๐Ÿฆ ๐Ÿบ ๐Ÿ’ง ๐ŸŒณ ๐Ÿ—ผ ๐ŸŽฐ"], + ["๐Ÿ˜€","faces", "๐Ÿ˜€ ๐Ÿ˜ƒ ๐Ÿ˜„ ๐Ÿ˜ ๐Ÿ˜† ๐Ÿ˜… ๐Ÿคฃ ๐Ÿ˜‚ ๐Ÿ™‚ ๐Ÿ™ƒ ๐Ÿ˜‰ ๐Ÿ˜Š ๐Ÿ˜‡ ๐Ÿฅฐ ๐Ÿ˜ ๐Ÿคฉ ๐Ÿ˜˜ ๐Ÿ˜— ๐Ÿ˜š ๐Ÿ˜™ ๐Ÿฅฒ ๐Ÿ˜‹ ๐Ÿ˜› ๐Ÿ˜œ ๐Ÿคช ๐Ÿ˜ ๐Ÿค— ๐Ÿคญ ๐Ÿคซ ๐Ÿค” ๐Ÿคจ ๐Ÿ˜ ๐Ÿ˜‘ ๐Ÿ˜ถ ๐Ÿ˜ ๐Ÿ˜’ ๐Ÿ™„ ๐Ÿ˜ฌ ๐Ÿ˜ฎโ€๐Ÿ’จ ๐Ÿคฅ ๐Ÿ˜Œ ๐Ÿ˜” ๐Ÿ˜ช ๐Ÿคค ๐Ÿ˜ด ๐Ÿฅฑ ๐Ÿ˜ตโ€๐Ÿ’ซ ๐Ÿค ๐Ÿฅด ๐Ÿคข ๐Ÿคฎ ๐Ÿคง ๐Ÿฅต ๐Ÿฅถ ๐Ÿ˜Ž ๐Ÿค“ ๐Ÿง ๐Ÿ˜• ๐Ÿ˜Ÿ ๐Ÿ™ ๐Ÿ˜ฎ ๐Ÿ˜ฏ ๐Ÿ˜ฒ ๐Ÿ˜ณ ๐Ÿฅบ ๐Ÿ˜ฆ ๐Ÿ˜ง ๐Ÿ˜จ ๐Ÿ˜ฐ ๐Ÿ˜ฅ ๐Ÿ˜ข ๐Ÿ˜ญ ๐Ÿ˜ฑ ๐Ÿ˜– ๐Ÿ˜ฃ ๐Ÿ˜ž ๐Ÿ˜“ ๐Ÿ˜ฉ ๐Ÿ˜ซ ๐Ÿ˜ค ๐Ÿ˜ก ๐Ÿ˜  ๐Ÿคฌ ๐Ÿ˜ˆ ๐Ÿ’€ ๐Ÿคก"], + ["๐Ÿซถ","hands", "๐Ÿ‘‹ ๐Ÿคš ๐Ÿ–๏ธ โœ‹ ๐Ÿ–– ๐Ÿ‘Œ ๐ŸคŒ ๐Ÿค โœŒ๏ธ ๐Ÿคž ๐Ÿซฐ ๐ŸคŸ ๐Ÿค˜ ๐Ÿค™ ๐Ÿ‘ˆ ๐Ÿ‘‰ ๐Ÿ‘† ๐Ÿ–• ๐Ÿ‘‡ โ˜๏ธ ๐Ÿ‘ ๐Ÿ‘Ž โœŠ ๐Ÿ‘Š ๐Ÿค› ๐Ÿคœ ๐Ÿ‘ ๐Ÿ™Œ ๐Ÿซถ ๐Ÿ‘ ๐Ÿคฒ ๐Ÿค ๐Ÿ™ โœ๏ธ ๐Ÿ’… ๐Ÿคณ ๐Ÿ’ช ๐Ÿฆพ ๐Ÿฆต ๐Ÿฆถ ๐Ÿ‘€ ๐Ÿ‘๏ธ ๐Ÿง  ๐Ÿซ€ ๐Ÿซ ๐Ÿฆท ๐Ÿ‘… ๐Ÿ‘„"], + ["๐Ÿˆ","animals","๐Ÿถ ๐Ÿฑ ๐Ÿญ ๐Ÿน ๐Ÿฐ ๐ŸฆŠ ๐Ÿป ๐Ÿผ ๐Ÿจ ๐Ÿฏ ๐Ÿฆ ๐Ÿฎ ๐Ÿท ๐Ÿธ ๐Ÿต ๐Ÿ™ˆ ๐Ÿ™‰ ๐Ÿ™Š ๐Ÿ” ๐Ÿง ๐Ÿฆ ๐Ÿค ๐Ÿฆ† ๐Ÿฆ‰ ๐Ÿฆ‡ ๐Ÿบ ๐Ÿ— ๐Ÿด ๐Ÿฆ„ ๐Ÿ ๐Ÿชฒ ๐Ÿฆ‹ ๐ŸŒ ๐Ÿž ๐Ÿœ ๐Ÿ•ท๏ธ ๐Ÿฆ‚ ๐Ÿข ๐Ÿ ๐ŸฆŽ ๐Ÿ™ ๐Ÿฆ‘ ๐Ÿฆ ๐Ÿฆ€ ๐Ÿก ๐Ÿ  ๐ŸŸ ๐Ÿฌ ๐Ÿณ ๐Ÿ‹ ๐Ÿฆˆ ๐ŸŠ ๐Ÿ… ๐Ÿฆ“ ๐Ÿฆ ๐Ÿ˜ ๐Ÿฆ› ๐Ÿซ ๐Ÿฆ’ ๐Ÿฆ˜ ๐Ÿˆ ๐Ÿ• ๐Ÿ•Š๏ธ"], + ["๐Ÿœ","food", "๐Ÿ ๐ŸŽ ๐Ÿ ๐ŸŠ ๐Ÿ‹ ๐ŸŒ ๐Ÿ‰ ๐Ÿ‡ ๐Ÿ“ ๐Ÿซ ๐Ÿˆ ๐Ÿ’ ๐Ÿ‘ ๐Ÿฅญ ๐Ÿ ๐Ÿฅฅ ๐Ÿฅ ๐Ÿ… ๐Ÿฅ‘ ๐Ÿฅฆ ๐Ÿฅ• ๐ŸŒฝ ๐ŸŒถ๏ธ ๐Ÿฅ” ๐Ÿ  ๐Ÿฅ ๐Ÿž ๐Ÿฅ– ๐Ÿง€ ๐Ÿฅš ๐Ÿณ ๐Ÿฅž ๐Ÿง‡ ๐Ÿฅ“ ๐Ÿ” ๐ŸŸ ๐Ÿ• ๐ŸŒญ ๐Ÿฅช ๐ŸŒฎ ๐ŸŒฏ ๐Ÿฅ™ ๐Ÿœ ๐Ÿฒ ๐Ÿ› ๐Ÿฃ ๐Ÿฑ ๐ŸฅŸ ๐Ÿค ๐Ÿ™ ๐Ÿš ๐Ÿฅ ๐Ÿฅ  ๐Ÿฆ ๐Ÿฐ ๐ŸŽ‚ ๐Ÿซ ๐Ÿฌ ๐Ÿญ ๐Ÿฎ โ˜• ๐Ÿต ๐Ÿง‹ ๐Ÿฅค ๐Ÿบ ๐Ÿป ๐Ÿฅ‚ ๐Ÿท ๐Ÿฅƒ"], + ["๐Ÿš€","things", "โŒจ๏ธ ๐Ÿ–ฅ๏ธ ๐Ÿ–ฑ๏ธ ๐Ÿ’ป ๐Ÿ“ฑ ๐Ÿ•น๏ธ ๐Ÿ’พ ๐Ÿ’ฟ ๐Ÿ“€ ๐Ÿ”Œ ๐Ÿ”‹ ๐Ÿชซ ๐Ÿ“ท ๐ŸŽฅ ๐Ÿ“บ ๐Ÿ“ป ๐ŸŽ™๏ธ ๐ŸŽš๏ธ ๐ŸŽ›๏ธ โฑ๏ธ โฐ ๐Ÿ•ฐ๏ธ ๐Ÿ” ๐Ÿ”Ž ๐Ÿ”’ ๐Ÿ”“ ๐Ÿ”‘ ๐Ÿ—๏ธ ๐Ÿ”จ ๐Ÿช“ โ›๏ธ ๐Ÿ› ๏ธ ๐Ÿ—ก๏ธ โš”๏ธ ๐Ÿ”ง ๐Ÿช› ๐Ÿ”ฉ โš™๏ธ ๐Ÿงฐ ๐Ÿงฒ ๐Ÿ’ฃ ๐Ÿงจ ๐Ÿชค ๐Ÿงช ๐Ÿงฌ ๐Ÿ”ฌ ๐Ÿ”ญ ๐Ÿ“ก ๐Ÿ’‰ ๐Ÿ’Š ๐Ÿš€ ๐Ÿ›ธ โœˆ๏ธ ๐Ÿš ๐Ÿš— ๐Ÿ๏ธ ๐Ÿšฒ โ›ต ๐Ÿ›ฐ๏ธ"], + ["๐ŸŒ","nature", "๐ŸŒ ๐ŸŒŽ ๐ŸŒ ๐ŸŒ‘ ๐ŸŒ’ ๐ŸŒ“ ๐ŸŒ” ๐ŸŒ• ๐ŸŒ– ๐ŸŒ— ๐ŸŒ˜ ๐ŸŒ™ ๐ŸŒš ๐ŸŒ› ๐ŸŒœ โ˜€๏ธ ๐ŸŒ ๐ŸŒž โญ ๐ŸŒŸ ๐Ÿ’ซ โœจ โ˜„๏ธ ๐Ÿช ๐ŸŒŒ โ˜๏ธ โ›… ๐ŸŒค๏ธ ๐ŸŒฅ๏ธ ๐ŸŒฆ๏ธ ๐ŸŒง๏ธ โ›ˆ๏ธ ๐ŸŒฉ๏ธ ๐ŸŒจ๏ธ โ„๏ธ โ˜ƒ๏ธ ๐ŸŒฌ๏ธ ๐ŸŒช๏ธ ๐ŸŒซ๏ธ ๐ŸŒŠ ๐Ÿ’ง ๐Ÿ”ฅ ๐ŸŒˆ ๐ŸŒธ ๐ŸŒบ ๐ŸŒป ๐ŸŒผ ๐ŸŒท ๐ŸŒน ๐Ÿฅ€ ๐ŸŒฟ ๐Ÿ€ ๐Ÿ ๐Ÿ‚ ๐ŸŒพ ๐ŸŒต ๐ŸŒฒ ๐ŸŒณ ๐ŸŒด"], + ["๐Ÿ”ฃ","symbols","โค๏ธ ๐Ÿงก ๐Ÿ’› ๐Ÿ’š ๐Ÿ’™ ๐Ÿ’œ ๐Ÿ–ค ๐Ÿค ๐ŸคŽ ๐Ÿ’” โฃ๏ธ ๐Ÿ’• ๐Ÿ’ž ๐Ÿ’“ ๐Ÿ’— ๐Ÿ’– ๐Ÿ’˜ ๐Ÿ’ โœ… โŒ โญ• ๐Ÿšซ โ›” โš ๏ธ ๐Ÿ”ด ๐ŸŸ  ๐ŸŸก ๐ŸŸข ๐Ÿ”ต ๐ŸŸฃ โšซ โšช ๐Ÿ”บ ๐Ÿ”ป ๐Ÿ”ถ ๐Ÿ”ท โฌ› โฌœ โ–ถ๏ธ โธ๏ธ โน๏ธ ๐Ÿ” ๐Ÿ”‚ ๐Ÿ”€ โž• โž– โœ–๏ธ โž— โ™พ๏ธ โ“ โ— ๐Ÿ’ฏ ๐Ÿ”” ๐Ÿ”• โณ โŒ› ๐ŸŽฏ ๐Ÿ† ๐Ÿฅ‡ ๐ŸŽ ๐ŸŽ‰"], +]; +const RECENT_KEY = "ccsidepanel-emoji-recent", RECENT_MAX = 24; + +const emojiPad = document.getElementById("emoji-pad"); +const emojiBtn = document.getElementById("emoji-btn"); +const emojiCats = document.getElementById("emoji-cats"); +const emojiGrid = document.getElementById("emoji-grid"); +const emojiLbl = document.getElementById("emoji-lbl"); + +function recents() { + try { return JSON.parse(localStorage.getItem(RECENT_KEY)) || []; } catch (e) { return []; } +} +function remember(ch) { + const list = [ch, ...recents().filter(x => x !== ch)].slice(0, RECENT_MAX); + try { localStorage.setItem(RECENT_KEY, JSON.stringify(list)); } catch (e) {} +} + +function insertEmoji(ch) { + // Insert at the caret, not at the end โ€” appending mangles a half-typed line. + const start = textEl.selectionStart ?? textEl.value.length; + const end = textEl.selectionEnd ?? textEl.value.length; + textEl.value = textEl.value.slice(0, start) + ch + textEl.value.slice(end); + const caret = start + ch.length; + textEl.focus(); + textEl.setSelectionRange(caret, caret); +} + +function paintEmoji(list, label) { + emojiGrid.innerHTML = ""; + emojiLbl.textContent = list.length ? label : `${label} ยท nothing yet`; + for (const ch of list) { + const b = document.createElement("button"); + b.type = "button"; + b.textContent = ch; + b.addEventListener("click", () => { insertEmoji(ch); remember(ch); }); // pad stays open + emojiGrid.appendChild(b); + } +} + +function showCat(i) { + [...emojiCats.children].forEach((b, k) => b.classList.toggle("on", k === i)); + if (i === 0) return paintEmoji(recents(), "recent"); + const [, name, set] = EMOJI[i - 1]; + paintEmoji(set.split(" ").filter(Boolean), name); +} + +// tab 0 is recents, then one per category +[["๐Ÿ•˜", "recent"], ...EMOJI.map(c => [c[0], c[1]])].forEach(([icon, name], i) => { + const b = document.createElement("button"); + b.type = "button"; + b.textContent = icon; + b.title = name; + b.addEventListener("click", () => showCat(i)); + emojiCats.appendChild(b); +}); + +function toggleEmoji(show) { + emojiPad.hidden = show === undefined ? !emojiPad.hidden : !show; + if (!emojiPad.hidden) { showCat(recents().length ? 0 : 1); textEl.focus(); } +} + +emojiBtn.addEventListener("click", (e) => { e.stopPropagation(); toggleEmoji(); }); +document.addEventListener("click", (e) => { + if (!emojiPad.hidden && !emojiPad.contains(e.target)) toggleEmoji(false); +}); +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && !emojiPad.hidden) toggleEmoji(false); +}); + sendBtn.addEventListener("click", sendText); textEl.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendText(); }