calabasas 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ import {
2
+ build_default,
3
+ relativeTime
4
+ } from "./index-8gymgyxd.js";
5
+ import"./index-tre7d3f1.js";
6
+ import {
7
+ __toESM
8
+ } from "./index-sdksp5px.js";
9
+ import {
10
+ Box_default,
11
+ Text
12
+ } from "./index-4rn9k8et.js";
13
+ import {
14
+ cliApi,
15
+ require_jsx_dev_runtime,
16
+ useQuery
17
+ } from "./convex-1z1jsz1n.js";
18
+ import"./index-vmy4gfe1.js";
19
+
20
+ // src/components/StatusBadge.tsx
21
+ var jsx_dev_runtime = __toESM(require_jsx_dev_runtime(), 1);
22
+ var STATUS_CONFIG = {
23
+ connected: { color: "green", dot: "●" },
24
+ connecting: { color: "yellow", dot: "●" },
25
+ error: { color: "red", dot: "●" },
26
+ disconnected: { color: "gray", dot: "●" }
27
+ };
28
+ function StatusBadge({ status, width = 0 }) {
29
+ const config = STATUS_CONFIG[status];
30
+ const label = `${config.dot} ${status}`;
31
+ const padded = width > 0 ? label.padEnd(width) : label;
32
+ return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
33
+ children: [
34
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
35
+ color: config.color,
36
+ children: config.dot
37
+ }, undefined, false, undefined, this),
38
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
39
+ children: padded.slice(1)
40
+ }, undefined, false, undefined, this)
41
+ ]
42
+ }, undefined, true, undefined, this);
43
+ }
44
+
45
+ // src/components/BotList.tsx
46
+ var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
47
+ function BotList({
48
+ apiKey,
49
+ selectedIndex,
50
+ onSelect
51
+ }) {
52
+ const bots = useQuery(cliApi.listBots, { apiKey });
53
+ if (bots === undefined) {
54
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
55
+ children: [
56
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
57
+ color: "cyan",
58
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(build_default, {
59
+ type: "dots"
60
+ }, undefined, false, undefined, this)
61
+ }, undefined, false, undefined, this),
62
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
63
+ children: " Loading bots..."
64
+ }, undefined, false, undefined, this)
65
+ ]
66
+ }, undefined, true, undefined, this);
67
+ }
68
+ if (bots.length === 0) {
69
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
70
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71
+ dimColor: true,
72
+ children: "No bots found. Run `calabasas bot add` to create one."
73
+ }, undefined, false, undefined, this)
74
+ }, undefined, false, undefined, this);
75
+ }
76
+ const COL = { name: 30, status: 16, appId: 22, env: 8, last: 16 };
77
+ function truncate(str, max) {
78
+ if (str.length <= max)
79
+ return str.padEnd(max);
80
+ return str.slice(0, max - 1) + "…";
81
+ }
82
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
83
+ flexDirection: "column",
84
+ children: [
85
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
86
+ marginBottom: 1,
87
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
88
+ bold: true,
89
+ children: [
90
+ " ",
91
+ "Name".padEnd(COL.name),
92
+ "Status".padEnd(COL.status),
93
+ "Discord App ID".padEnd(COL.appId),
94
+ "Env".padEnd(COL.env),
95
+ "Last Connected"
96
+ ]
97
+ }, undefined, true, undefined, this)
98
+ }, undefined, false, undefined, this),
99
+ bots.map((bot, i) => {
100
+ const isSelected = selectedIndex === i;
101
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
102
+ flexDirection: "column",
103
+ children: [
104
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
105
+ children: [
106
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
107
+ color: isSelected ? "cyan" : undefined,
108
+ children: isSelected ? "→ " : " "
109
+ }, undefined, false, undefined, this),
110
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
111
+ bold: isSelected,
112
+ children: truncate(bot.name, COL.name)
113
+ }, undefined, false, undefined, this),
114
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(StatusBadge, {
115
+ status: bot.status,
116
+ width: COL.status
117
+ }, undefined, false, undefined, this),
118
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
119
+ dimColor: true,
120
+ children: bot.discordAppId.padEnd(COL.appId)
121
+ }, undefined, false, undefined, this),
122
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
123
+ dimColor: true,
124
+ children: bot.environment.padEnd(COL.env)
125
+ }, undefined, false, undefined, this),
126
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
127
+ dimColor: true,
128
+ children: relativeTime(bot.lastConnectedAt)
129
+ }, undefined, false, undefined, this)
130
+ ]
131
+ }, undefined, true, undefined, this),
132
+ bot.errorMessage && /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
133
+ marginLeft: 4,
134
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
135
+ color: "red",
136
+ dimColor: true,
137
+ children: [
138
+ "└",
139
+ " ",
140
+ bot.errorMessage
141
+ ]
142
+ }, undefined, true, undefined, this)
143
+ }, undefined, false, undefined, this),
144
+ !bot.errorMessage && bot.disconnectReason && /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
145
+ marginLeft: 4,
146
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
147
+ dimColor: true,
148
+ children: [
149
+ "└",
150
+ " ",
151
+ bot.disconnectReason
152
+ ]
153
+ }, undefined, true, undefined, this)
154
+ }, undefined, false, undefined, this)
155
+ ]
156
+ }, bot._id, true, undefined, this);
157
+ })
158
+ ]
159
+ }, undefined, true, undefined, this);
160
+ }
161
+ export {
162
+ BotList
163
+ };
164
+
165
+ ;
@@ -0,0 +1,235 @@
1
+ import {
2
+ LogViewer
3
+ } from "./index-b0cp0nch.js";
4
+ import {
5
+ getConvexUrl,
6
+ resolveEnv,
7
+ resolvePlatformApiKey
8
+ } from "./index-a8vtmtf9.js";
9
+ import {
10
+ build_default,
11
+ formatLatency,
12
+ formatNumber
13
+ } from "./index-8gymgyxd.js";
14
+ import"./index-tre7d3f1.js";
15
+ import {
16
+ __toESM
17
+ } from "./index-sdksp5px.js";
18
+ import {
19
+ BotList
20
+ } from "./BotList-gmtf52xh.js";
21
+ import {
22
+ Box_default,
23
+ Text,
24
+ render_default,
25
+ use_app_default,
26
+ use_input_default
27
+ } from "./index-4rn9k8et.js";
28
+ import {
29
+ ConvexProvider,
30
+ cliApi,
31
+ createConvexClient,
32
+ require_jsx_dev_runtime,
33
+ useQuery
34
+ } from "./convex-1z1jsz1n.js";
35
+ import {
36
+ require_react
37
+ } from "./index-vmy4gfe1.js";
38
+
39
+ // src/components/Dashboard.tsx
40
+ var import_react2 = __toESM(require_react(), 1);
41
+
42
+ // src/components/Header.tsx
43
+ var jsx_dev_runtime = __toESM(require_jsx_dev_runtime(), 1);
44
+ function Header({
45
+ botCount,
46
+ env
47
+ }) {
48
+ return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Box_default, {
49
+ marginBottom: 1,
50
+ children: [
51
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
52
+ bold: true,
53
+ color: "magenta",
54
+ children: "calabasas"
55
+ }, undefined, false, undefined, this),
56
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
57
+ dimColor: true,
58
+ children: " v0.1.12"
59
+ }, undefined, false, undefined, this),
60
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
61
+ dimColor: true,
62
+ children: " · "
63
+ }, undefined, false, undefined, this),
64
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
65
+ children: [
66
+ botCount,
67
+ " bot",
68
+ botCount !== 1 ? "s" : ""
69
+ ]
70
+ }, undefined, true, undefined, this),
71
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
72
+ dimColor: true,
73
+ children: " · "
74
+ }, undefined, false, undefined, this),
75
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
76
+ color: env === "dev" ? "yellow" : "green",
77
+ children: env
78
+ }, undefined, false, undefined, this),
79
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
80
+ dimColor: true,
81
+ children: " · Press "
82
+ }, undefined, false, undefined, this),
83
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
84
+ bold: true,
85
+ children: "q"
86
+ }, undefined, false, undefined, this),
87
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
88
+ dimColor: true,
89
+ children: " to quit"
90
+ }, undefined, false, undefined, this)
91
+ ]
92
+ }, undefined, true, undefined, this);
93
+ }
94
+
95
+ // src/components/StatsPanel.tsx
96
+ var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
97
+ var ONE_HOUR = 60 * 60 * 1000;
98
+ function StatsPanel({
99
+ apiKey,
100
+ botId
101
+ }) {
102
+ const stats = useQuery(cliApi.botStats, {
103
+ apiKey,
104
+ botId,
105
+ since: Date.now() - ONE_HOUR
106
+ });
107
+ if (stats === undefined) {
108
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
109
+ children: [
110
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
111
+ color: "cyan",
112
+ children: /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(build_default, {
113
+ type: "dots"
114
+ }, undefined, false, undefined, this)
115
+ }, undefined, false, undefined, this),
116
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
117
+ children: " Loading stats..."
118
+ }, undefined, false, undefined, this)
119
+ ]
120
+ }, undefined, true, undefined, this);
121
+ }
122
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
123
+ gap: 2,
124
+ marginBottom: 1,
125
+ children: [
126
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
127
+ dimColor: true,
128
+ children: "Events (1h):"
129
+ }, undefined, false, undefined, this),
130
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
131
+ bold: true,
132
+ children: formatNumber(stats.total)
133
+ }, undefined, false, undefined, this),
134
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
135
+ color: "green",
136
+ children: [
137
+ formatNumber(stats.success),
138
+ " ok"
139
+ ]
140
+ }, undefined, true, undefined, this),
141
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
142
+ color: "red",
143
+ children: [
144
+ formatNumber(stats.failed),
145
+ " failed"
146
+ ]
147
+ }, undefined, true, undefined, this),
148
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
149
+ dimColor: true,
150
+ children: [
151
+ "avg ",
152
+ formatLatency(stats.avgLatencyMs)
153
+ ]
154
+ }, undefined, true, undefined, this)
155
+ ]
156
+ }, undefined, true, undefined, this);
157
+ }
158
+
159
+ // src/components/Dashboard.tsx
160
+ var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
161
+ function Dashboard({
162
+ apiKey,
163
+ env
164
+ }) {
165
+ const { exit } = use_app_default();
166
+ const [selectedIndex, setSelectedIndex] = import_react2.useState(0);
167
+ const bots = useQuery(cliApi.listBots, { apiKey });
168
+ const botCount = bots?.length ?? 0;
169
+ const selectedBot = bots?.[selectedIndex];
170
+ use_input_default(import_react2.useCallback((input, key) => {
171
+ if (input === "q") {
172
+ exit();
173
+ return;
174
+ }
175
+ if ((input === "j" || key.downArrow) && botCount > 0) {
176
+ setSelectedIndex((i) => Math.min(i, botCount - 1) === botCount - 1 ? 0 : i + 1);
177
+ }
178
+ if ((input === "k" || key.upArrow) && botCount > 0) {
179
+ setSelectedIndex((i) => i === 0 ? botCount - 1 : i - 1);
180
+ }
181
+ }, [botCount, exit]));
182
+ return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
183
+ flexDirection: "column",
184
+ children: [
185
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Header, {
186
+ botCount,
187
+ env
188
+ }, undefined, false, undefined, this),
189
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(BotList, {
190
+ apiKey,
191
+ selectedIndex
192
+ }, undefined, false, undefined, this),
193
+ selectedBot && /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
194
+ flexDirection: "column",
195
+ marginTop: 1,
196
+ children: [
197
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(StatsPanel, {
198
+ apiKey,
199
+ botId: selectedBot._id
200
+ }, undefined, false, undefined, this),
201
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(LogViewer, {
202
+ apiKey,
203
+ botId: selectedBot._id
204
+ }, undefined, false, undefined, this)
205
+ ]
206
+ }, undefined, true, undefined, this)
207
+ ]
208
+ }, undefined, true, undefined, this);
209
+ }
210
+
211
+ // src/commands/dashboard.tsx
212
+ var jsx_dev_runtime4 = __toESM(require_jsx_dev_runtime(), 1);
213
+ async function dashboard(options) {
214
+ const env = resolveEnv(options);
215
+ const resolved = resolvePlatformApiKey({}, env);
216
+ if (!resolved) {
217
+ console.log("Not logged in. Run `calabasas login` first.");
218
+ return;
219
+ }
220
+ const apiKey = resolved.key;
221
+ const convexUrl = getConvexUrl(env);
222
+ const client = createConvexClient(convexUrl);
223
+ const { waitUntilExit } = render_default(/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(ConvexProvider, {
224
+ client,
225
+ children: /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Dashboard, {
226
+ apiKey,
227
+ env
228
+ }, undefined, false, undefined, this)
229
+ }, undefined, false, undefined, this));
230
+ await waitUntilExit();
231
+ await client.close();
232
+ }
233
+ export {
234
+ dashboard
235
+ };
package/dist/index.js CHANGED
@@ -2204,7 +2204,7 @@ export default defineCalabasas({
2204
2204
  members: false, // Sync members (calabasasMembers table) - requires privileged intent
2205
2205
  },
2206
2206
 
2207
- // Custom event handlers - events forwarded to your discord:receive mutation
2207
+ // Custom event handlers - events forwarded to your calabasas/discord:receive mutation
2208
2208
  events: {
2209
2209
  // Enable events you want to receive (all fields included)
2210
2210
  // messageCreate: true,
@@ -2573,13 +2573,13 @@ async function generate(options) {
2573
2573
  export { ${syncExports.join(", ")} } from "./calabasas/_generated/sync";
2574
2574
 
2575
2575
  3. Add CALABASAS_SECRET to your Convex environment variables
2576
- 4. Create your event handler in convex/discord.ts
2576
+ 4. Create your event handler in convex/calabasas/discord.ts
2577
2577
  5. Run \`calabasas push\` to sync config with Calabasas`, "Next steps");
2578
2578
  } else {
2579
2579
  p5.note(`1. Add CALABASAS_SECRET to your Convex environment variables
2580
- 2. Create your handler in convex/discord.ts:
2580
+ 2. Create your handler in convex/calabasas/discord.ts:
2581
2581
 
2582
- import { handleDiscordEvent } from "./calabasas/_generated/discord";
2582
+ import { handleDiscordEvent } from "./_generated/discord";
2583
2583
 
2584
2584
  export const receive = handleDiscordEvent({
2585
2585
  messageCreate: async (ctx, event) => {
@@ -4729,6 +4729,878 @@ export function useOnlineCount(guildDiscordId: string): number | undefined {
4729
4729
  });`
4730
4730
  };
4731
4731
 
4732
+ // src/lib/registry/components/emoji-picker.ts
4733
+ var emojiPicker = {
4734
+ name: "emoji-picker",
4735
+ kind: "component",
4736
+ description: "Grid-based emoji picker with custom Discord emojis, Unicode emojis, search, and animated GIF support",
4737
+ requiredSyncTypes: ["emojis"],
4738
+ requiredShadcnComponents: ["popover", "button"],
4739
+ generateReactComponent: () => `"use client";
4740
+
4741
+ import { useState, useRef, useEffect, useCallback, useMemo } from "react";
4742
+ import { useQuery } from "convex/react";
4743
+ import { api } from "@/convex/_generated/api";
4744
+ import {
4745
+ Popover,
4746
+ PopoverContent,
4747
+ PopoverTrigger,
4748
+ } from "@/components/ui/popover";
4749
+ import { Button } from "@/components/ui/button";
4750
+ import { cn } from "@/lib/utils";
4751
+
4752
+ type EmojiData = {
4753
+ id: string;
4754
+ name: string;
4755
+ animated?: boolean;
4756
+ source: "guild" | "application" | "default";
4757
+ };
4758
+
4759
+ type EmojiPickerProps = {
4760
+ guildDiscordId?: string;
4761
+ source?: "all" | "guild" | "application" | "default";
4762
+ mode?: "single" | "multi";
4763
+ maxCount?: number;
4764
+ value?: string[];
4765
+ onSelect?: (emoji: EmojiData) => void;
4766
+ onChange?: (emojis: EmojiData[]) => void;
4767
+ columns?: number;
4768
+ pageSize?: number;
4769
+ placeholder?: string;
4770
+ className?: string;
4771
+ };
4772
+
4773
+ /** Curated default Unicode emojis organized by Discord-style categories */
4774
+ const DEFAULT_EMOJIS: { category: string; emojis: { codepoint: string; name: string }[] }[] = [
4775
+ {
4776
+ category: "Smileys & Emotion",
4777
+ emojis: [
4778
+ { codepoint: "\uD83D\uDE00", name: "grinning face" },
4779
+ { codepoint: "\uD83D\uDE03", name: "grinning face with big eyes" },
4780
+ { codepoint: "\uD83D\uDE04", name: "grinning face with smiling eyes" },
4781
+ { codepoint: "\uD83D\uDE01", name: "beaming face with smiling eyes" },
4782
+ { codepoint: "\uD83D\uDE06", name: "grinning squinting face" },
4783
+ { codepoint: "\uD83D\uDE05", name: "grinning face with sweat" },
4784
+ { codepoint: "\uD83E\uDD23", name: "rolling on the floor laughing" },
4785
+ { codepoint: "\uD83D\uDE02", name: "face with tears of joy" },
4786
+ { codepoint: "\uD83D\uDE42", name: "slightly smiling face" },
4787
+ { codepoint: "\uD83D\uDE09", name: "winking face" },
4788
+ { codepoint: "\uD83D\uDE0A", name: "smiling face with smiling eyes" },
4789
+ { codepoint: "\uD83D\uDE07", name: "smiling face with halo" },
4790
+ { codepoint: "\uD83E\uDD70", name: "smiling face with hearts" },
4791
+ { codepoint: "\uD83D\uDE0D", name: "smiling face with heart eyes" },
4792
+ { codepoint: "\uD83E\uDD29", name: "star struck" },
4793
+ { codepoint: "\uD83D\uDE18", name: "face blowing a kiss" },
4794
+ { codepoint: "\uD83D\uDE0B", name: "face savoring food" },
4795
+ { codepoint: "\uD83D\uDE1B", name: "face with tongue" },
4796
+ { codepoint: "\uD83D\uDE1C", name: "winking face with tongue" },
4797
+ { codepoint: "\uD83E\uDD2A", name: "zany face" },
4798
+ { codepoint: "\uD83D\uDE1D", name: "squinting face with tongue" },
4799
+ { codepoint: "\uD83E\uDD11", name: "money mouth face" },
4800
+ { codepoint: "\uD83E\uDD17", name: "hugging face" },
4801
+ { codepoint: "\uD83E\uDD2D", name: "face with hand over mouth" },
4802
+ { codepoint: "\uD83E\uDD2B", name: "shushing face" },
4803
+ { codepoint: "\uD83E\uDD14", name: "thinking face" },
4804
+ { codepoint: "\uD83E\uDD10", name: "zipper mouth face" },
4805
+ { codepoint: "\uD83E\uDD28", name: "face with raised eyebrow" },
4806
+ { codepoint: "\uD83D\uDE10", name: "neutral face" },
4807
+ { codepoint: "\uD83D\uDE11", name: "expressionless face" },
4808
+ { codepoint: "\uD83D\uDE36", name: "face without mouth" },
4809
+ { codepoint: "\uD83D\uDE0F", name: "smirking face" },
4810
+ { codepoint: "\uD83D\uDE12", name: "unamused face" },
4811
+ { codepoint: "\uD83D\uDE44", name: "face with rolling eyes" },
4812
+ { codepoint: "\uD83D\uDE2C", name: "grimacing face" },
4813
+ { codepoint: "\uD83E\uDD25", name: "lying face" },
4814
+ { codepoint: "\uD83D\uDE0C", name: "relieved face" },
4815
+ { codepoint: "\uD83D\uDE14", name: "pensive face" },
4816
+ { codepoint: "\uD83D\uDE2A", name: "sleepy face" },
4817
+ { codepoint: "\uD83E\uDD24", name: "drooling face" },
4818
+ { codepoint: "\uD83D\uDE34", name: "sleeping face" },
4819
+ { codepoint: "\uD83D\uDE37", name: "face with medical mask" },
4820
+ { codepoint: "\uD83E\uDD12", name: "face with thermometer" },
4821
+ { codepoint: "\uD83E\uDD15", name: "face with head bandage" },
4822
+ { codepoint: "\uD83E\uDD22", name: "nauseated face" },
4823
+ { codepoint: "\uD83E\uDD2E", name: "face vomiting" },
4824
+ { codepoint: "\uD83E\uDD75", name: "hot face" },
4825
+ { codepoint: "\uD83E\uDD76", name: "cold face" },
4826
+ { codepoint: "\uD83E\uDD74", name: "woozy face" },
4827
+ { codepoint: "\uD83D\uDE35", name: "face with crossed out eyes" },
4828
+ { codepoint: "\uD83E\uDD2F", name: "exploding head" },
4829
+ { codepoint: "\uD83D\uDE0E", name: "smiling face with sunglasses" },
4830
+ { codepoint: "\uD83E\uDD73", name: "partying face" },
4831
+ { codepoint: "\uD83D\uDE1F", name: "worried face" },
4832
+ { codepoint: "\uD83D\uDE15", name: "confused face" },
4833
+ { codepoint: "\uD83D\uDE22", name: "crying face" },
4834
+ { codepoint: "\uD83D\uDE2D", name: "loudly crying face" },
4835
+ { codepoint: "\uD83D\uDE24", name: "face with steam from nose" },
4836
+ { codepoint: "\uD83D\uDE20", name: "angry face" },
4837
+ { codepoint: "\uD83D\uDE21", name: "pouting face" },
4838
+ { codepoint: "\uD83E\uDD2C", name: "face with symbols on mouth" },
4839
+ { codepoint: "\uD83D\uDC7F", name: "angry face with horns" },
4840
+ { codepoint: "\uD83D\uDC80", name: "skull" },
4841
+ { codepoint: "\uD83D\uDCA9", name: "pile of poo" },
4842
+ { codepoint: "\uD83E\uDD21", name: "clown face" },
4843
+ { codepoint: "\uD83D\uDC7B", name: "ghost" },
4844
+ { codepoint: "\uD83D\uDC7D", name: "alien" },
4845
+ { codepoint: "\uD83E\uDD16", name: "robot" },
4846
+ { codepoint: "\uD83D\uDE3A", name: "grinning cat" },
4847
+ { codepoint: "\uD83D\uDE3B", name: "smiling cat with heart eyes" },
4848
+ { codepoint: "\uD83D\uDC8B", name: "kiss mark" },
4849
+ { codepoint: "\uD83D\uDCAF", name: "hundred points" },
4850
+ { codepoint: "\uD83D\uDCA2", name: "anger symbol" },
4851
+ { codepoint: "\uD83D\uDCA5", name: "collision" },
4852
+ { codepoint: "\uD83D\uDCAB", name: "dizzy" },
4853
+ { codepoint: "\uD83D\uDCA6", name: "sweat droplets" },
4854
+ { codepoint: "❤️", name: "red heart" },
4855
+ { codepoint: "\uD83E\uDDE1", name: "orange heart" },
4856
+ { codepoint: "\uD83D\uDC9B", name: "yellow heart" },
4857
+ { codepoint: "\uD83D\uDC9A", name: "green heart" },
4858
+ { codepoint: "\uD83D\uDC99", name: "blue heart" },
4859
+ { codepoint: "\uD83D\uDC9C", name: "purple heart" },
4860
+ { codepoint: "\uD83D\uDDA4", name: "black heart" },
4861
+ { codepoint: "\uD83E\uDD0D", name: "white heart" },
4862
+ { codepoint: "\uD83D\uDC94", name: "broken heart" },
4863
+ ],
4864
+ },
4865
+ {
4866
+ category: "People & Body",
4867
+ emojis: [
4868
+ { codepoint: "\uD83D\uDC4B", name: "waving hand" },
4869
+ { codepoint: "\uD83E\uDD1A", name: "raised back of hand" },
4870
+ { codepoint: "✋", name: "raised hand" },
4871
+ { codepoint: "\uD83D\uDD96", name: "vulcan salute" },
4872
+ { codepoint: "\uD83D\uDC4C", name: "ok hand" },
4873
+ { codepoint: "\uD83E\uDD0C", name: "pinched fingers" },
4874
+ { codepoint: "✌️", name: "victory hand" },
4875
+ { codepoint: "\uD83E\uDD1E", name: "crossed fingers" },
4876
+ { codepoint: "\uD83E\uDD1F", name: "love you gesture" },
4877
+ { codepoint: "\uD83E\uDD18", name: "sign of the horns" },
4878
+ { codepoint: "\uD83E\uDD19", name: "call me hand" },
4879
+ { codepoint: "\uD83D\uDC48", name: "backhand index pointing left" },
4880
+ { codepoint: "\uD83D\uDC49", name: "backhand index pointing right" },
4881
+ { codepoint: "\uD83D\uDC46", name: "backhand index pointing up" },
4882
+ { codepoint: "\uD83D\uDC47", name: "backhand index pointing down" },
4883
+ { codepoint: "☝️", name: "index pointing up" },
4884
+ { codepoint: "\uD83D\uDC4D", name: "thumbs up" },
4885
+ { codepoint: "\uD83D\uDC4E", name: "thumbs down" },
4886
+ { codepoint: "✊", name: "raised fist" },
4887
+ { codepoint: "\uD83D\uDC4A", name: "oncoming fist" },
4888
+ { codepoint: "\uD83E\uDD1B", name: "left facing fist" },
4889
+ { codepoint: "\uD83E\uDD1C", name: "right facing fist" },
4890
+ { codepoint: "\uD83D\uDC4F", name: "clapping hands" },
4891
+ { codepoint: "\uD83D\uDE4C", name: "raising hands" },
4892
+ { codepoint: "\uD83D\uDC50", name: "open hands" },
4893
+ { codepoint: "\uD83E\uDD32", name: "palms up together" },
4894
+ { codepoint: "\uD83E\uDD1D", name: "handshake" },
4895
+ { codepoint: "\uD83D\uDE4F", name: "folded hands" },
4896
+ { codepoint: "\uD83D\uDCAA", name: "flexed biceps" },
4897
+ { codepoint: "\uD83E\uDDBE", name: "mechanical arm" },
4898
+ { codepoint: "\uD83D\uDC40", name: "eyes" },
4899
+ { codepoint: "\uD83D\uDC41️", name: "eye" },
4900
+ { codepoint: "\uD83D\uDC45", name: "tongue" },
4901
+ { codepoint: "\uD83D\uDC44", name: "mouth" },
4902
+ { codepoint: "\uD83E\uDDE0", name: "brain" },
4903
+ { codepoint: "\uD83D\uDC76", name: "baby" },
4904
+ { codepoint: "\uD83E\uDDD1", name: "person" },
4905
+ { codepoint: "\uD83D\uDC66", name: "boy" },
4906
+ { codepoint: "\uD83D\uDC67", name: "girl" },
4907
+ ],
4908
+ },
4909
+ {
4910
+ category: "Animals & Nature",
4911
+ emojis: [
4912
+ { codepoint: "\uD83D\uDC36", name: "dog face" },
4913
+ { codepoint: "\uD83D\uDC31", name: "cat face" },
4914
+ { codepoint: "\uD83D\uDC2D", name: "mouse face" },
4915
+ { codepoint: "\uD83D\uDC39", name: "hamster" },
4916
+ { codepoint: "\uD83D\uDC30", name: "rabbit face" },
4917
+ { codepoint: "\uD83E\uDD8A", name: "fox" },
4918
+ { codepoint: "\uD83D\uDC3B", name: "bear" },
4919
+ { codepoint: "\uD83D\uDC3C", name: "panda" },
4920
+ { codepoint: "\uD83D\uDC28", name: "koala" },
4921
+ { codepoint: "\uD83D\uDC2F", name: "tiger face" },
4922
+ { codepoint: "\uD83E\uDD81", name: "lion" },
4923
+ { codepoint: "\uD83D\uDC2E", name: "cow face" },
4924
+ { codepoint: "\uD83D\uDC37", name: "pig face" },
4925
+ { codepoint: "\uD83D\uDC38", name: "frog" },
4926
+ { codepoint: "\uD83D\uDC35", name: "monkey face" },
4927
+ { codepoint: "\uD83D\uDE48", name: "see no evil monkey" },
4928
+ { codepoint: "\uD83D\uDE49", name: "hear no evil monkey" },
4929
+ { codepoint: "\uD83D\uDE4A", name: "speak no evil monkey" },
4930
+ { codepoint: "\uD83D\uDC14", name: "chicken" },
4931
+ { codepoint: "\uD83D\uDC27", name: "penguin" },
4932
+ { codepoint: "\uD83D\uDC26", name: "bird" },
4933
+ { codepoint: "\uD83E\uDD85", name: "eagle" },
4934
+ { codepoint: "\uD83E\uDD86", name: "duck" },
4935
+ { codepoint: "\uD83E\uDD89", name: "owl" },
4936
+ { codepoint: "\uD83D\uDC3A", name: "wolf" },
4937
+ { codepoint: "\uD83D\uDC17", name: "boar" },
4938
+ { codepoint: "\uD83D\uDC34", name: "horse face" },
4939
+ { codepoint: "\uD83E\uDD84", name: "unicorn" },
4940
+ { codepoint: "\uD83D\uDC1D", name: "honeybee" },
4941
+ { codepoint: "\uD83D\uDC1B", name: "bug" },
4942
+ { codepoint: "\uD83E\uDD8B", name: "butterfly" },
4943
+ { codepoint: "\uD83D\uDC0C", name: "snail" },
4944
+ { codepoint: "\uD83D\uDC19", name: "octopus" },
4945
+ { codepoint: "\uD83D\uDC20", name: "tropical fish" },
4946
+ { codepoint: "\uD83D\uDC2C", name: "dolphin" },
4947
+ { codepoint: "\uD83D\uDC33", name: "spouting whale" },
4948
+ { codepoint: "\uD83E\uDD88", name: "shark" },
4949
+ { codepoint: "\uD83D\uDC0A", name: "crocodile" },
4950
+ { codepoint: "\uD83D\uDC09", name: "dragon" },
4951
+ { codepoint: "\uD83C\uDF38", name: "cherry blossom" },
4952
+ { codepoint: "\uD83C\uDF39", name: "rose" },
4953
+ { codepoint: "\uD83C\uDF3A", name: "hibiscus" },
4954
+ { codepoint: "\uD83C\uDF3B", name: "sunflower" },
4955
+ { codepoint: "\uD83C\uDF32", name: "evergreen tree" },
4956
+ { codepoint: "\uD83C\uDF35", name: "cactus" },
4957
+ { codepoint: "\uD83C\uDF40", name: "four leaf clover" },
4958
+ { codepoint: "\uD83C\uDF41", name: "maple leaf" },
4959
+ { codepoint: "\uD83C\uDF42", name: "fallen leaf" },
4960
+ { codepoint: "\uD83C\uDF43", name: "leaf fluttering in wind" },
4961
+ ],
4962
+ },
4963
+ {
4964
+ category: "Food & Drink",
4965
+ emojis: [
4966
+ { codepoint: "\uD83C\uDF4E", name: "red apple" },
4967
+ { codepoint: "\uD83C\uDF4A", name: "tangerine" },
4968
+ { codepoint: "\uD83C\uDF4B", name: "lemon" },
4969
+ { codepoint: "\uD83C\uDF4C", name: "banana" },
4970
+ { codepoint: "\uD83C\uDF49", name: "watermelon" },
4971
+ { codepoint: "\uD83C\uDF47", name: "grapes" },
4972
+ { codepoint: "\uD83C\uDF53", name: "strawberry" },
4973
+ { codepoint: "\uD83E\uDED0", name: "blueberries" },
4974
+ { codepoint: "\uD83C\uDF51", name: "peach" },
4975
+ { codepoint: "\uD83C\uDF52", name: "cherries" },
4976
+ { codepoint: "\uD83E\uDD51", name: "avocado" },
4977
+ { codepoint: "\uD83C\uDF55", name: "pizza" },
4978
+ { codepoint: "\uD83C\uDF54", name: "hamburger" },
4979
+ { codepoint: "\uD83C\uDF5F", name: "french fries" },
4980
+ { codepoint: "\uD83C\uDF2D", name: "hot dog" },
4981
+ { codepoint: "\uD83C\uDF7F", name: "popcorn" },
4982
+ { codepoint: "\uD83C\uDF69", name: "doughnut" },
4983
+ { codepoint: "\uD83C\uDF6A", name: "cookie" },
4984
+ { codepoint: "\uD83C\uDF82", name: "birthday cake" },
4985
+ { codepoint: "\uD83C\uDF70", name: "shortcake" },
4986
+ { codepoint: "\uD83E\uDDC1", name: "cupcake" },
4987
+ { codepoint: "\uD83C\uDF6B", name: "chocolate bar" },
4988
+ { codepoint: "\uD83C\uDF6C", name: "candy" },
4989
+ { codepoint: "☕", name: "hot beverage" },
4990
+ { codepoint: "\uD83C\uDF75", name: "teacup without handle" },
4991
+ { codepoint: "\uD83C\uDF7A", name: "beer mug" },
4992
+ { codepoint: "\uD83C\uDF7B", name: "clinking beer mugs" },
4993
+ { codepoint: "\uD83E\uDD42", name: "clinking glasses" },
4994
+ { codepoint: "\uD83C\uDF77", name: "wine glass" },
4995
+ { codepoint: "\uD83E\uDDC3", name: "beverage box" },
4996
+ ],
4997
+ },
4998
+ {
4999
+ category: "Travel & Places",
5000
+ emojis: [
5001
+ { codepoint: "\uD83D\uDE97", name: "automobile" },
5002
+ { codepoint: "\uD83D\uDE95", name: "taxi" },
5003
+ { codepoint: "\uD83D\uDE80", name: "rocket" },
5004
+ { codepoint: "✈️", name: "airplane" },
5005
+ { codepoint: "\uD83D\uDE81", name: "helicopter" },
5006
+ { codepoint: "\uD83D\uDE82", name: "locomotive" },
5007
+ { codepoint: "\uD83D\uDEA2", name: "ship" },
5008
+ { codepoint: "\uD83C\uDFE0", name: "house" },
5009
+ { codepoint: "\uD83C\uDFE2", name: "office building" },
5010
+ { codepoint: "\uD83C\uDFF0", name: "castle" },
5011
+ { codepoint: "\uD83D\uDDFC", name: "Tokyo tower" },
5012
+ { codepoint: "\uD83D\uDDFD", name: "Statue of Liberty" },
5013
+ { codepoint: "⛪", name: "church" },
5014
+ { codepoint: "\uD83C\uDF0D", name: "globe showing Europe Africa" },
5015
+ { codepoint: "\uD83C\uDF0E", name: "globe showing Americas" },
5016
+ { codepoint: "\uD83C\uDF0F", name: "globe showing Asia Australia" },
5017
+ { codepoint: "\uD83C\uDF0B", name: "volcano" },
5018
+ { codepoint: "\uD83C\uDFD4️", name: "snow capped mountain" },
5019
+ { codepoint: "⛰️", name: "mountain" },
5020
+ { codepoint: "\uD83C\uDFD6️", name: "beach with umbrella" },
5021
+ { codepoint: "\uD83C\uDF05", name: "sunrise" },
5022
+ { codepoint: "\uD83C\uDF04", name: "sunrise over mountains" },
5023
+ { codepoint: "\uD83C\uDF20", name: "shooting star" },
5024
+ { codepoint: "\uD83C\uDF87", name: "sparkler" },
5025
+ { codepoint: "\uD83C\uDF86", name: "fireworks" },
5026
+ { codepoint: "\uD83C\uDF08", name: "rainbow" },
5027
+ { codepoint: "⛈️", name: "cloud with lightning and rain" },
5028
+ { codepoint: "❄️", name: "snowflake" },
5029
+ { codepoint: "☀️", name: "sun" },
5030
+ { codepoint: "\uD83C\uDF19", name: "crescent moon" },
5031
+ { codepoint: "⭐", name: "star" },
5032
+ { codepoint: "\uD83C\uDF1F", name: "glowing star" },
5033
+ { codepoint: "\uD83D\uDCA7", name: "droplet" },
5034
+ { codepoint: "\uD83D\uDD25", name: "fire" },
5035
+ ],
5036
+ },
5037
+ {
5038
+ category: "Activities",
5039
+ emojis: [
5040
+ { codepoint: "⚽", name: "soccer ball" },
5041
+ { codepoint: "\uD83C\uDFC0", name: "basketball" },
5042
+ { codepoint: "\uD83C\uDFC8", name: "american football" },
5043
+ { codepoint: "⚾", name: "baseball" },
5044
+ { codepoint: "\uD83C\uDFBE", name: "tennis" },
5045
+ { codepoint: "\uD83C\uDFD0", name: "volleyball" },
5046
+ { codepoint: "\uD83C\uDFB1", name: "pool 8 ball" },
5047
+ { codepoint: "\uD83C\uDFD3", name: "ping pong" },
5048
+ { codepoint: "\uD83C\uDFAE", name: "video game" },
5049
+ { codepoint: "\uD83D\uDD79️", name: "joystick" },
5050
+ { codepoint: "\uD83C\uDFB2", name: "game die" },
5051
+ { codepoint: "\uD83E\uDDE9", name: "puzzle piece" },
5052
+ { codepoint: "\uD83C\uDFAF", name: "bullseye" },
5053
+ { codepoint: "\uD83C\uDFB3", name: "bowling" },
5054
+ { codepoint: "\uD83C\uDFAA", name: "circus tent" },
5055
+ { codepoint: "\uD83C\uDFAD", name: "performing arts" },
5056
+ { codepoint: "\uD83C\uDFA8", name: "artist palette" },
5057
+ { codepoint: "\uD83C\uDFAC", name: "clapper board" },
5058
+ { codepoint: "\uD83C\uDFA4", name: "microphone" },
5059
+ { codepoint: "\uD83C\uDFA7", name: "headphone" },
5060
+ { codepoint: "\uD83C\uDFB5", name: "musical note" },
5061
+ { codepoint: "\uD83C\uDFB6", name: "musical notes" },
5062
+ { codepoint: "\uD83C\uDFB9", name: "musical keyboard" },
5063
+ { codepoint: "\uD83C\uDFB8", name: "guitar" },
5064
+ { codepoint: "\uD83E\uDD41", name: "drum" },
5065
+ { codepoint: "\uD83C\uDFAA", name: "circus tent" },
5066
+ { codepoint: "\uD83C\uDFC6", name: "trophy" },
5067
+ { codepoint: "\uD83E\uDD47", name: "1st place medal" },
5068
+ { codepoint: "\uD83E\uDD48", name: "2nd place medal" },
5069
+ { codepoint: "\uD83E\uDD49", name: "3rd place medal" },
5070
+ { codepoint: "\uD83C\uDFC5", name: "sports medal" },
5071
+ { codepoint: "\uD83C\uDF96️", name: "military medal" },
5072
+ { codepoint: "\uD83C\uDF97️", name: "reminder ribbon" },
5073
+ { codepoint: "\uD83C\uDF81", name: "wrapped gift" },
5074
+ { codepoint: "\uD83C\uDF88", name: "balloon" },
5075
+ { codepoint: "\uD83C\uDF89", name: "party popper" },
5076
+ { codepoint: "\uD83C\uDF8A", name: "confetti ball" },
5077
+ ],
5078
+ },
5079
+ {
5080
+ category: "Objects",
5081
+ emojis: [
5082
+ { codepoint: "⌚", name: "watch" },
5083
+ { codepoint: "\uD83D\uDCF1", name: "mobile phone" },
5084
+ { codepoint: "\uD83D\uDCBB", name: "laptop" },
5085
+ { codepoint: "⌨️", name: "keyboard" },
5086
+ { codepoint: "\uD83D\uDDA5️", name: "desktop computer" },
5087
+ { codepoint: "\uD83D\uDDA8️", name: "printer" },
5088
+ { codepoint: "\uD83D\uDDB1️", name: "computer mouse" },
5089
+ { codepoint: "\uD83D\uDCBE", name: "floppy disk" },
5090
+ { codepoint: "\uD83D\uDCBF", name: "optical disk" },
5091
+ { codepoint: "\uD83D\uDCF7", name: "camera" },
5092
+ { codepoint: "\uD83D\uDCF9", name: "video camera" },
5093
+ { codepoint: "\uD83C\uDFA5", name: "movie camera" },
5094
+ { codepoint: "\uD83D\uDCFA", name: "television" },
5095
+ { codepoint: "\uD83D\uDCFB", name: "radio" },
5096
+ { codepoint: "\uD83D\uDD14", name: "bell" },
5097
+ { codepoint: "\uD83D\uDCE3", name: "megaphone" },
5098
+ { codepoint: "\uD83D\uDCA1", name: "light bulb" },
5099
+ { codepoint: "\uD83D\uDD26", name: "flashlight" },
5100
+ { codepoint: "\uD83D\uDCDA", name: "books" },
5101
+ { codepoint: "\uD83D\uDCD6", name: "open book" },
5102
+ { codepoint: "\uD83D\uDCDD", name: "memo" },
5103
+ { codepoint: "✏️", name: "pencil" },
5104
+ { codepoint: "\uD83D\uDCCE", name: "paperclip" },
5105
+ { codepoint: "\uD83D\uDCCC", name: "pushpin" },
5106
+ { codepoint: "\uD83D\uDD11", name: "key" },
5107
+ { codepoint: "\uD83D\uDD12", name: "locked" },
5108
+ { codepoint: "\uD83D\uDD13", name: "unlocked" },
5109
+ { codepoint: "\uD83D\uDD27", name: "wrench" },
5110
+ { codepoint: "\uD83D\uDD28", name: "hammer" },
5111
+ { codepoint: "⚙️", name: "gear" },
5112
+ { codepoint: "\uD83E\uDDF2", name: "magnet" },
5113
+ { codepoint: "\uD83D\uDC8E", name: "gem stone" },
5114
+ { codepoint: "\uD83E\uDDEA", name: "test tube" },
5115
+ { codepoint: "\uD83D\uDC8A", name: "pill" },
5116
+ { codepoint: "\uD83E\uDE79", name: "adhesive bandage" },
5117
+ ],
5118
+ },
5119
+ {
5120
+ category: "Symbols",
5121
+ emojis: [
5122
+ { codepoint: "❤️", name: "red heart" },
5123
+ { codepoint: "✅", name: "check mark button" },
5124
+ { codepoint: "❌", name: "cross mark" },
5125
+ { codepoint: "⭕", name: "hollow red circle" },
5126
+ { codepoint: "❗", name: "red exclamation mark" },
5127
+ { codepoint: "❓", name: "red question mark" },
5128
+ { codepoint: "⚠️", name: "warning" },
5129
+ { codepoint: "\uD83D\uDD34", name: "red circle" },
5130
+ { codepoint: "\uD83D\uDFE0", name: "orange circle" },
5131
+ { codepoint: "\uD83D\uDFE1", name: "yellow circle" },
5132
+ { codepoint: "\uD83D\uDFE2", name: "green circle" },
5133
+ { codepoint: "\uD83D\uDD35", name: "blue circle" },
5134
+ { codepoint: "\uD83D\uDFE3", name: "purple circle" },
5135
+ { codepoint: "⚫", name: "black circle" },
5136
+ { codepoint: "⚪", name: "white circle" },
5137
+ { codepoint: "\uD83D\uDD36", name: "large orange diamond" },
5138
+ { codepoint: "\uD83D\uDD37", name: "large blue diamond" },
5139
+ { codepoint: "➕", name: "plus" },
5140
+ { codepoint: "➖", name: "minus" },
5141
+ { codepoint: "➗", name: "divide" },
5142
+ { codepoint: "✖️", name: "multiply" },
5143
+ { codepoint: "♻️", name: "recycling symbol" },
5144
+ { codepoint: "\uD83D\uDCB2", name: "heavy dollar sign" },
5145
+ { codepoint: "©️", name: "copyright" },
5146
+ { codepoint: "®️", name: "registered" },
5147
+ { codepoint: "™️", name: "trade mark" },
5148
+ { codepoint: "\uD83C\uDFF3️", name: "white flag" },
5149
+ { codepoint: "\uD83C\uDFF4", name: "black flag" },
5150
+ { codepoint: "\uD83D\uDEA9", name: "triangular flag" },
5151
+ { codepoint: "\uD83C\uDF8C", name: "crossed flags" },
5152
+ ],
5153
+ },
5154
+ ];
5155
+
5156
+ /** Renders a custom Discord emoji with animated hover behavior */
5157
+ function CustomEmoji({
5158
+ discordId,
5159
+ name,
5160
+ animated,
5161
+ selected,
5162
+ size = 32,
5163
+ }: {
5164
+ discordId: string;
5165
+ name: string;
5166
+ animated?: boolean;
5167
+ selected?: boolean;
5168
+ size?: number;
5169
+ }) {
5170
+ const [hovered, setHovered] = useState(false);
5171
+ const ext = animated && (hovered || selected) ? "gif" : "png";
5172
+ const url = \`https://cdn.discordapp.com/emojis/\${discordId}.\${ext}?size=\${size}&quality=lossless\`;
5173
+
5174
+ return (
5175
+ <img
5176
+ src={url}
5177
+ alt={name}
5178
+ title={name}
5179
+ width={size}
5180
+ height={size}
5181
+ loading="lazy"
5182
+ className="object-contain"
5183
+ onMouseEnter={() => setHovered(true)}
5184
+ onMouseLeave={() => setHovered(false)}
5185
+ />
5186
+ );
5187
+ }
5188
+
5189
+ export function EmojiPicker({
5190
+ guildDiscordId,
5191
+ source = "all",
5192
+ mode = "single",
5193
+ maxCount,
5194
+ value,
5195
+ onSelect,
5196
+ onChange,
5197
+ columns = 8,
5198
+ pageSize = 64,
5199
+ placeholder = "Pick emoji...",
5200
+ className,
5201
+ }: EmojiPickerProps) {
5202
+ const [open, setOpen] = useState(false);
5203
+ const [search, setSearch] = useState("");
5204
+ const [selected, setSelected] = useState<EmojiData[]>([]);
5205
+ const [visibleCount, setVisibleCount] = useState(pageSize);
5206
+ const sentinelRef = useRef<HTMLDivElement>(null);
5207
+ const scrollRef = useRef<HTMLDivElement>(null);
5208
+ const searchRef = useRef<HTMLInputElement>(null);
5209
+
5210
+ const currentSelected = value
5211
+ ? selected.filter((e) => value.includes(e.id))
5212
+ : selected;
5213
+
5214
+ // Determine which data sources to fetch
5215
+ const needsGuild = source === "all" || source === "guild";
5216
+ const needsApp = source === "all" || source === "application";
5217
+ const needsDefault = source === "all" || source === "default";
5218
+
5219
+ const guildEmojis = useQuery(
5220
+ api.calabasas.queries.listGuildEmojis,
5221
+ needsGuild && guildDiscordId ? { guildDiscordId } : "skip"
5222
+ );
5223
+ const appEmojis = useQuery(
5224
+ api.calabasas.queries.listAppEmojis,
5225
+ needsApp ? {} : "skip"
5226
+ );
5227
+
5228
+ // Build flat emoji list for search and infinite scroll
5229
+ const allEmojis = useMemo(() => {
5230
+ const sections: { header: string; items: EmojiData[] }[] = [];
5231
+
5232
+ if (needsGuild && guildEmojis && guildEmojis.length > 0) {
5233
+ sections.push({
5234
+ header: "Server Emojis",
5235
+ items: guildEmojis.map((e) => ({
5236
+ id: e.discordId,
5237
+ name: e.name,
5238
+ animated: e.animated,
5239
+ source: "guild" as const,
5240
+ })),
5241
+ });
5242
+ }
5243
+
5244
+ if (needsApp && appEmojis && appEmojis.length > 0) {
5245
+ sections.push({
5246
+ header: "App Emojis",
5247
+ items: appEmojis.map((e) => ({
5248
+ id: e.discordId,
5249
+ name: e.name,
5250
+ animated: e.animated,
5251
+ source: "application" as const,
5252
+ })),
5253
+ });
5254
+ }
5255
+
5256
+ if (needsDefault) {
5257
+ for (const cat of DEFAULT_EMOJIS) {
5258
+ sections.push({
5259
+ header: cat.category,
5260
+ items: cat.emojis.map((e) => ({
5261
+ id: e.codepoint,
5262
+ name: e.name,
5263
+ source: "default" as const,
5264
+ })),
5265
+ });
5266
+ }
5267
+ }
5268
+
5269
+ return sections;
5270
+ }, [guildEmojis, appEmojis, needsGuild, needsApp, needsDefault]);
5271
+
5272
+ // Filter by search
5273
+ const filtered = useMemo(() => {
5274
+ if (!search) return allEmojis;
5275
+ const q = search.toLowerCase();
5276
+ const flat = allEmojis.flatMap((s) => s.items).filter((e) =>
5277
+ e.name.toLowerCase().includes(q)
5278
+ );
5279
+ return flat.length > 0 ? [{ header: "", items: flat }] : [];
5280
+ }, [allEmojis, search]);
5281
+
5282
+ // Flatten for counting
5283
+ const totalCount = useMemo(
5284
+ () => filtered.reduce((acc, s) => acc + s.items.length, 0),
5285
+ [filtered]
5286
+ );
5287
+
5288
+ // Intersection observer for infinite scroll
5289
+ useEffect(() => {
5290
+ const sentinel = sentinelRef.current;
5291
+ if (!sentinel) return;
5292
+
5293
+ const observer = new IntersectionObserver(
5294
+ ([entry]) => {
5295
+ if (entry.isIntersecting && visibleCount < totalCount) {
5296
+ setVisibleCount((prev) => prev + pageSize);
5297
+ }
5298
+ },
5299
+ { root: scrollRef.current, threshold: 0 }
5300
+ );
5301
+
5302
+ observer.observe(sentinel);
5303
+ return () => observer.disconnect();
5304
+ }, [visibleCount, totalCount, pageSize]);
5305
+
5306
+ // Reset visible count on search change
5307
+ useEffect(() => {
5308
+ setVisibleCount(pageSize);
5309
+ }, [search, pageSize]);
5310
+
5311
+ // Auto-focus search on open
5312
+ useEffect(() => {
5313
+ if (open) {
5314
+ setTimeout(() => searchRef.current?.focus(), 0);
5315
+ } else {
5316
+ setSearch("");
5317
+ }
5318
+ }, [open]);
5319
+
5320
+ const isSelected = useCallback(
5321
+ (id: string) => {
5322
+ if (value) return value.includes(id);
5323
+ return currentSelected.some((e) => e.id === id);
5324
+ },
5325
+ [value, currentSelected]
5326
+ );
5327
+
5328
+ const handleSelect = useCallback(
5329
+ (emoji: EmojiData) => {
5330
+ if (mode === "single") {
5331
+ setSelected([emoji]);
5332
+ onSelect?.(emoji);
5333
+ onChange?.([emoji]);
5334
+ setOpen(false);
5335
+ return;
5336
+ }
5337
+
5338
+ // Multi mode
5339
+ const alreadySelected = isSelected(emoji.id);
5340
+ let next: EmojiData[];
5341
+
5342
+ if (alreadySelected) {
5343
+ next = currentSelected.filter((e) => e.id !== emoji.id);
5344
+ } else {
5345
+ if (maxCount && currentSelected.length >= maxCount) return;
5346
+ next = [...currentSelected, emoji];
5347
+ }
5348
+
5349
+ setSelected(next);
5350
+ onSelect?.(emoji);
5351
+ onChange?.(next);
5352
+ },
5353
+ [mode, maxCount, currentSelected, isSelected, onSelect, onChange]
5354
+ );
5355
+
5356
+ const handleClear = useCallback(() => {
5357
+ setSelected([]);
5358
+ onChange?.([]);
5359
+ }, [onChange]);
5360
+
5361
+ const atMax = mode === "multi" && maxCount !== undefined && currentSelected.length >= maxCount;
5362
+
5363
+ // Render grid items with virtual slicing
5364
+ let rendered = 0;
5365
+ const gridStyle = { gridTemplateColumns: \`repeat(\${columns}, 1fr)\` };
5366
+
5367
+ // Trigger display
5368
+ const renderTrigger = () => {
5369
+ if (currentSelected.length === 0) return placeholder;
5370
+
5371
+ if (mode === "single" && currentSelected.length === 1) {
5372
+ const emoji = currentSelected[0];
5373
+ if (emoji.source === "default") {
5374
+ return <span className="text-xl">{emoji.id}</span>;
5375
+ }
5376
+ return (
5377
+ <CustomEmoji
5378
+ discordId={emoji.id}
5379
+ name={emoji.name}
5380
+ animated={emoji.animated}
5381
+ selected
5382
+ size={20}
5383
+ />
5384
+ );
5385
+ }
5386
+
5387
+ // Multi mode
5388
+ const shown = currentSelected.slice(0, 5);
5389
+ const extra = currentSelected.length - shown.length;
5390
+ return (
5391
+ <span className="flex items-center gap-1">
5392
+ {shown.map((emoji) =>
5393
+ emoji.source === "default" ? (
5394
+ <span key={emoji.id} className="text-base">{emoji.id}</span>
5395
+ ) : (
5396
+ <CustomEmoji
5397
+ key={emoji.id}
5398
+ discordId={emoji.id}
5399
+ name={emoji.name}
5400
+ animated={emoji.animated}
5401
+ selected
5402
+ size={18}
5403
+ />
5404
+ )
5405
+ )}
5406
+ {extra > 0 && (
5407
+ <span className="text-xs text-muted-foreground ml-1">
5408
+ +{extra}
5409
+ </span>
5410
+ )}
5411
+ </span>
5412
+ );
5413
+ };
5414
+
5415
+ return (
5416
+ <Popover open={open} onOpenChange={setOpen}>
5417
+ <PopoverTrigger asChild>
5418
+ <Button
5419
+ variant="outline"
5420
+ role="combobox"
5421
+ aria-expanded={open}
5422
+ className={cn("w-[280px] justify-between", className)}
5423
+ >
5424
+ {renderTrigger()}
5425
+ <span className="ml-2 text-lg opacity-50">\uD83D\uDE00</span>
5426
+ </Button>
5427
+ </PopoverTrigger>
5428
+ <PopoverContent className="w-[352px] p-0" align="start">
5429
+ {/* Search */}
5430
+ <div className="flex items-center gap-2 border-b px-3 py-2">
5431
+ <svg
5432
+ className="h-4 w-4 shrink-0 opacity-50"
5433
+ fill="none"
5434
+ stroke="currentColor"
5435
+ viewBox="0 0 24 24"
5436
+ >
5437
+ <path
5438
+ strokeLinecap="round"
5439
+ strokeLinejoin="round"
5440
+ strokeWidth={2}
5441
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
5442
+ />
5443
+ </svg>
5444
+ <input
5445
+ ref={searchRef}
5446
+ type="text"
5447
+ placeholder="Search emojis..."
5448
+ value={search}
5449
+ onChange={(e) => setSearch(e.target.value)}
5450
+ className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
5451
+ />
5452
+ {search && (
5453
+ <button
5454
+ onClick={() => setSearch("")}
5455
+ className="text-muted-foreground hover:text-foreground"
5456
+ >
5457
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5458
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
5459
+ </svg>
5460
+ </button>
5461
+ )}
5462
+ </div>
5463
+
5464
+ {/* Scrollable emoji grid */}
5465
+ <div
5466
+ ref={scrollRef}
5467
+ className="max-h-[400px] overflow-y-auto p-2"
5468
+ >
5469
+ {filtered.length === 0 && (
5470
+ <div className="py-8 text-center text-sm text-muted-foreground">
5471
+ No emojis found
5472
+ </div>
5473
+ )}
5474
+
5475
+ {filtered.map((section) => {
5476
+ if (rendered >= visibleCount) return null;
5477
+
5478
+ const remaining = visibleCount - rendered;
5479
+ const items = section.items.slice(0, remaining);
5480
+ rendered += items.length;
5481
+
5482
+ return (
5483
+ <div key={section.header || "search-results"}>
5484
+ {section.header && (
5485
+ <div className="sticky top-0 z-10 bg-popover px-1 py-1.5 text-xs font-semibold text-muted-foreground">
5486
+ {section.header}
5487
+ </div>
5488
+ )}
5489
+ <div className="grid gap-0.5" style={gridStyle}>
5490
+ {items.map((emoji) => {
5491
+ const sel = isSelected(emoji.id);
5492
+ const disabled = !sel && atMax;
5493
+
5494
+ return (
5495
+ <button
5496
+ key={\`\${emoji.source}-\${emoji.id}\`}
5497
+ onClick={() => !disabled && handleSelect(emoji)}
5498
+ title={emoji.name}
5499
+ className={cn(
5500
+ "flex items-center justify-center rounded-md p-1 h-9 w-full transition-colors",
5501
+ sel
5502
+ ? "ring-2 ring-primary bg-accent/50"
5503
+ : "hover:bg-accent",
5504
+ disabled && "opacity-50 cursor-not-allowed"
5505
+ )}
5506
+ >
5507
+ {emoji.source === "default" ? (
5508
+ <span className="text-xl leading-none">{emoji.id}</span>
5509
+ ) : (
5510
+ <CustomEmoji
5511
+ discordId={emoji.id}
5512
+ name={emoji.name}
5513
+ animated={emoji.animated}
5514
+ selected={sel}
5515
+ size={28}
5516
+ />
5517
+ )}
5518
+ </button>
5519
+ );
5520
+ })}
5521
+ </div>
5522
+ </div>
5523
+ );
5524
+ })}
5525
+
5526
+ {/* Sentinel for infinite scroll */}
5527
+ <div ref={sentinelRef} className="h-1" />
5528
+ </div>
5529
+
5530
+ {/* Multi-select footer */}
5531
+ {mode === "multi" && (
5532
+ <div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
5533
+ <span>
5534
+ Selected: {currentSelected.length}
5535
+ {maxCount !== undefined && \` / \${maxCount}\`}
5536
+ </span>
5537
+ {currentSelected.length > 0 && (
5538
+ <button
5539
+ onClick={handleClear}
5540
+ className="text-xs text-muted-foreground hover:text-foreground underline"
5541
+ >
5542
+ Clear
5543
+ </button>
5544
+ )}
5545
+ </div>
5546
+ )}
5547
+ </PopoverContent>
5548
+ </Popover>
5549
+ );
5550
+ }
5551
+ `,
5552
+ generateConvexQueries: () => `export const listGuildEmojis = query({
5553
+ args: { guildDiscordId: v.string() },
5554
+ returns: v.array(
5555
+ v.object({
5556
+ discordId: v.string(),
5557
+ name: v.string(),
5558
+ animated: v.boolean(),
5559
+ available: v.boolean(),
5560
+ })
5561
+ ),
5562
+ handler: async (ctx, { guildDiscordId }) => {
5563
+ const emojis = await ctx.db
5564
+ .query("calabasasEmojis")
5565
+ .withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
5566
+ .collect();
5567
+ return emojis
5568
+ .filter((e) => e.available)
5569
+ .map((e) => ({
5570
+ discordId: e.discordId,
5571
+ name: e.name,
5572
+ animated: e.animated,
5573
+ available: e.available,
5574
+ }));
5575
+ },
5576
+ });
5577
+
5578
+ export const listAppEmojis = query({
5579
+ args: {},
5580
+ returns: v.array(
5581
+ v.object({
5582
+ discordId: v.string(),
5583
+ name: v.string(),
5584
+ animated: v.boolean(),
5585
+ available: v.boolean(),
5586
+ })
5587
+ ),
5588
+ handler: async (ctx) => {
5589
+ const emojis = await ctx.db
5590
+ .query("calabasasAppEmojis")
5591
+ .collect();
5592
+ return emojis
5593
+ .filter((e) => e.available)
5594
+ .map((e) => ({
5595
+ discordId: e.discordId,
5596
+ name: e.name,
5597
+ animated: e.animated,
5598
+ available: e.available,
5599
+ }));
5600
+ },
5601
+ });`
5602
+ };
5603
+
4732
5604
  // src/lib/registry/index.ts
4733
5605
  var REGISTRY = [
4734
5606
  channelSelect,
@@ -4741,7 +5613,8 @@ var REGISTRY = [
4741
5613
  memberCard,
4742
5614
  channelTree,
4743
5615
  memberRoster,
4744
- useOnlineCount
5616
+ useOnlineCount,
5617
+ emojiPicker
4745
5618
  ];
4746
5619
  function getComponent(name) {
4747
5620
  return REGISTRY.find((c) => c.name === name);
@@ -4764,6 +5637,8 @@ function parseEnabledSyncTypes(configPath) {
4764
5637
  enabled.add("members");
4765
5638
  if (/presence:\s*true/.test(syncContent))
4766
5639
  enabled.add("presence");
5640
+ if (/emojis:\s*true/.test(syncContent))
5641
+ enabled.add("emojis");
4767
5642
  }
4768
5643
  return enabled;
4769
5644
  }
@@ -4925,7 +5800,15 @@ const count = ${camel}("123456789");`;
4925
5800
  "member-card"
4926
5801
  ]);
4927
5802
  const isDisplay = displayComponents.has(firstInstalled.name);
4928
- if (isDisplay) {
5803
+ const isEmojiPicker = firstInstalled.name === "emoji-picker";
5804
+ if (isEmojiPicker) {
5805
+ usage = `import { ${pascal} } from "@/components/calabasas/${firstInstalled.name}";
5806
+
5807
+ <${pascal}
5808
+ guildDiscordId="123456789"
5809
+ onSelect={(emoji) => console.log(emoji)}
5810
+ />`;
5811
+ } else if (isDisplay) {
4929
5812
  const propsMap = {
4930
5813
  "role-badge": ` guildDiscordId="123456789"
4931
5814
  roleIds={["role_id_1", "role_id_2"]}`,
@@ -5329,7 +6212,7 @@ async function botList(options) {
5329
6212
  const React = reactModule.default ?? reactModule;
5330
6213
  const { render } = await import("./index-4rn9k8et.js");
5331
6214
  const { createConvexClient, ConvexProvider } = await import("./convex-1z1jsz1n.js");
5332
- const { BotList } = await import("./BotList-pbt2yxmj.js");
6215
+ const { BotList } = await import("./BotList-gmtf52xh.js");
5333
6216
  const convexUrl = getConvexUrl(env);
5334
6217
  const client = createConvexClient(convexUrl);
5335
6218
  const { waitUntilExit } = render(React.createElement(ConvexProvider, {
@@ -5359,7 +6242,8 @@ Your Discord bots:
5359
6242
  console.log(`${statusDot} ${pc.bold(bot.name)}`);
5360
6243
  console.log(` ${pc.dim("ID:")} ${bot._id}`);
5361
6244
  console.log(` ${pc.dim("Discord App:")} ${bot.discordAppId}`);
5362
- console.log(` ${pc.dim("Status:")} ${statusColor(bot.status)}`);
6245
+ const statusText = (bot.status === "disconnected" || bot.status === "error") && bot.disconnectReason ? `${statusColor(bot.status)} ${pc.dim("")} ${pc.dim(bot.disconnectReason)}` : statusColor(bot.status);
6246
+ console.log(` ${pc.dim("Status:")} ${statusText}`);
5363
6247
  console.log(` ${pc.dim("Env:")} ${bot.environment}`);
5364
6248
  console.log("");
5365
6249
  }
@@ -6149,7 +7033,7 @@ Run ${pc3.cyan("`calabasas init`")} first.`);
6149
7033
 
6150
7034
  // src/index.ts
6151
7035
  var dashboard = async (...args) => {
6152
- const mod = await import("./dashboard-jhqez8y6.js");
7036
+ const mod = await import("./dashboard-y75f0avj.js");
6153
7037
  return mod.dashboard(args[0]);
6154
7038
  };
6155
7039
  var logs = async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "CLI for Calabasas - Discord Gateway as a Service for Convex",
5
5
  "type": "module",
6
6
  "bin": {