eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Emoji browser: `/emoji [search]` opens a COMMAND PANEL (the transient
2// results surface above the composer β€” see apps.js) with the matching emoji
3// from the curated shortcode set; tapping one drops it into your message and
4// dismisses the panel. The reference implementation for panels: no events, no
5// state, no thread of its own β€” the command and its temporary surface are the
6// whole app.
7import { defineApp } from "../apps.js";
8import { ICON_SMILE } from "../icons.js";
9import { EMOJI } from "../emoji.js";
10
11// searchEmoji filters the shortcode set by a substring of the name and
12// collapses aliases (laughing/satisfied are one glyph) to one tile each,
13// keeping the first name as the label. Pure, so it's unit-testable in Node.
14export function searchEmoji(query) {
15 const q = (query || "").trim().toLowerCase();
16 const seen = new Set();
17 const out = [];
18 for (const [name, ch] of Object.entries(EMOJI)) {
19 if (q && !name.includes(q)) continue;
20 if (seen.has(ch)) continue;
21 seen.add(ch);
22 out.push({ name, ch });
23 }
24 return out;
25}
26
27export const app = defineApp({
28 name: "emoji",
29 title: "Emoji",
30 glyph: "😊",
31 symbol: "face.smiling", // native chrome: the SF Symbols name iOS/Mac render (docs/icons.md)
32 logo: ICON_SMILE,
33 // Command-only: no personal thread to route to (the command acts where you
34 // typed it) and nothing to instantiate from the Apps menu.
35 places: [], // command-only utility β€” acts where you type, lives nowhere
36 initialState: () => ({}),
37 events: {},
38
39 commands: {
40 "/emoji": {
41 desc: "browse emoji β€” tap one to add it to your message",
42 args: "[search]",
43 run(args, ctx) {
44 const q = (args || "").trim();
45 const results = searchEmoji(q);
46 if (!results.length) {
47 ctx.reply({ text: `No emoji match β€œ${q}”. Try \`/emoji cat\`, or a bare \`/emoji\` to browse them all.` });
48 return;
49 }
50 const opened = ctx.panel({
51 title: q ? `Emoji matching β€œ${q}”` : "Emoji β€” tap to add",
52 render(el, panel) {
53 const grid = document.createElement("div");
54 grid.className = "emoji-grid";
55 for (const r of results) {
56 const b = document.createElement("button");
57 b.type = "button";
58 b.className = "emoji-grid-btn";
59 b.textContent = r.ch;
60 b.title = ":" + r.name + ":";
61 b.setAttribute("aria-label", r.name.replace(/_/g, " "));
62 b.onclick = () => {
63 panel.insertText(r.ch);
64 panel.close();
65 };
66 grid.appendChild(b);
67 }
68 el.appendChild(grid);
69 },
70 });
71 // No chat surface to draw a panel in (shouldn't happen where a
72 // composer exists, but stay honest): answer with a reply instead.
73 if (!opened) ctx.reply({ text: results.slice(0, 30).map((r) => r.ch).join(" ") });
74 },
75 },
76 },
77
78 styles: `
79.emoji-grid { display: flex; flex-wrap: wrap; gap: 2px; }
80.emoji-grid-btn {
81 border: none; background: none; font-size: 22px; line-height: 1;
82 padding: 6px; border-radius: 8px; cursor: pointer;
83}
84.emoji-grid-btn:hover { background: var(--bubble-them); }
85`,
86});
87