calabasas 0.16.0 → 0.16.1
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.
- package/dist/index.js +288 -148
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4733,7 +4733,7 @@ export function useOnlineCount(guildDiscordId: string): number | undefined {
|
|
|
4733
4733
|
var emojiPicker = {
|
|
4734
4734
|
name: "emoji-picker",
|
|
4735
4735
|
kind: "component",
|
|
4736
|
-
description: "
|
|
4736
|
+
description: "Discord-style emoji picker with category sidebar, lazy-loaded custom emojis, Unicode emojis, search, and animated GIF support",
|
|
4737
4737
|
requiredSyncTypes: ["emojis"],
|
|
4738
4738
|
requiredShadcnComponents: ["popover", "button"],
|
|
4739
4739
|
generateReactComponent: () => `"use client";
|
|
@@ -4765,11 +4765,64 @@ type EmojiPickerProps = {
|
|
|
4765
4765
|
onSelect?: (emoji: EmojiData) => void;
|
|
4766
4766
|
onChange?: (emojis: EmojiData[]) => void;
|
|
4767
4767
|
columns?: number;
|
|
4768
|
-
pageSize?: number;
|
|
4769
4768
|
placeholder?: string;
|
|
4770
4769
|
className?: string;
|
|
4771
4770
|
};
|
|
4772
4771
|
|
|
4772
|
+
/** Category icon SVGs for the sidebar (Discord-style) */
|
|
4773
|
+
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
|
4774
|
+
"Smileys & Emotion": (
|
|
4775
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4776
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5-6c.78 2.34 2.72 4 5 4s4.22-1.66 5-4H7zm1-2.5c.83 0 1.5-.67 1.5-1.5S8.83 8.5 8 8.5 6.5 9.17 6.5 10 7.17 11.5 8 11.5zm8 0c.83 0 1.5-.67 1.5-1.5S16.83 8.5 16 8.5s-1.5.67-1.5 1.5.67 1.5 1.5 1.5z"/>
|
|
4777
|
+
</svg>
|
|
4778
|
+
),
|
|
4779
|
+
"People & Body": (
|
|
4780
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4781
|
+
<path d="M7 7c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2zm-2 0c0 2.21 1.79 4 4 4s4-1.79 4-4-1.79-4-4-4-4 1.79-4 4zm7.22 4.22L9 14.44l-3.22-3.22L4.37 12.6l4.63 4.63 4.63-4.63-1.41-1.38zM17 7c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2zm-2 0c0 2.21 1.79 4 4 4s4-1.79 4-4-1.79-4-4-4-4 1.79-4 4z"/>
|
|
4782
|
+
</svg>
|
|
4783
|
+
),
|
|
4784
|
+
"Animals & Nature": (
|
|
4785
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4786
|
+
<path d="M6.05 8.05c-2.73 2.73-2.73 7.15-.02 9.88a6.95 6.95 0 004.95 2.05c.78 0 1.57-.13 2.32-.39-.98-.54-1.86-1.27-2.59-2.17-.35-.43-.66-.89-.93-1.37a6.93 6.93 0 01-.8-4.44c.12-.9.42-1.75.85-2.53a7.06 7.06 0 01-3.78-1.03zm4.97.17c-.36.42-.66.89-.9 1.39-.42.9-.63 1.87-.62 2.87.01.95.23 1.88.66 2.72.23.45.5.87.82 1.24.68.8 1.52 1.42 2.46 1.87 2.13-1.05 3.61-3.15 3.61-5.59 0-2.07-1.01-3.9-2.56-5.05a6.93 6.93 0 00-3.47 1.55zM19.95 2a6.97 6.97 0 00-4.93 2.05 7.1 7.1 0 00-.58.65A8.94 8.94 0 0119 10.72c0 3.4-1.94 6.35-4.77 7.82A8.98 8.98 0 0110 20a8.9 8.9 0 01-5.64-2 6.98 6.98 0 005.64 4 6.97 6.97 0 004.95-2.05c2.73-2.73 2.73-7.17 0-9.9.65-.56 1.2-1.23 1.63-1.99A6.93 6.93 0 0020 4c0-.7-.1-1.38-.3-2h.25z"/>
|
|
4787
|
+
</svg>
|
|
4788
|
+
),
|
|
4789
|
+
"Food & Drink": (
|
|
4790
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4791
|
+
<path d="M18.06 22.99h1.66c.84 0 1.53-.64 1.63-1.46L23 5.05h-5V1h-1.97v4.05h-4.97l.3 2.34c1.71.47 3.31 1.32 4.27 2.26 1.44 1.42 2.43 2.89 2.43 5.29v8.05zM1 21.99V21h15.03v.99c0 .55-.45 1-1.01 1H2.01c-.56 0-1.01-.45-1.01-1zm15.03-7c0-4.5-6.77-5-7.52-5s-7.51.5-7.51 5v1h15.03v-1zM1.02 17h15v2h-15v-2z"/>
|
|
4792
|
+
</svg>
|
|
4793
|
+
),
|
|
4794
|
+
"Travel & Places": (
|
|
4795
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4796
|
+
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
|
4797
|
+
</svg>
|
|
4798
|
+
),
|
|
4799
|
+
"Activities": (
|
|
4800
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4801
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z"/>
|
|
4802
|
+
</svg>
|
|
4803
|
+
),
|
|
4804
|
+
"Objects": (
|
|
4805
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4806
|
+
<path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6A4.997 4.997 0 017 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/>
|
|
4807
|
+
</svg>
|
|
4808
|
+
),
|
|
4809
|
+
"Symbols": (
|
|
4810
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4811
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
4812
|
+
</svg>
|
|
4813
|
+
),
|
|
4814
|
+
"Server Emojis": (
|
|
4815
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4816
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
4817
|
+
</svg>
|
|
4818
|
+
),
|
|
4819
|
+
"App Emojis": (
|
|
4820
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
4821
|
+
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-7-2h2v-4h4v-2h-4V7h-2v4H8v2h4z"/>
|
|
4822
|
+
</svg>
|
|
4823
|
+
),
|
|
4824
|
+
};
|
|
4825
|
+
|
|
4773
4826
|
/** Curated default Unicode emojis organized by Discord-style categories */
|
|
4774
4827
|
const DEFAULT_EMOJIS: { category: string; emojis: { codepoint: string; name: string }[] }[] = [
|
|
4775
4828
|
{
|
|
@@ -5153,21 +5206,30 @@ const DEFAULT_EMOJIS: { category: string; emojis: { codepoint: string; name: str
|
|
|
5153
5206
|
},
|
|
5154
5207
|
];
|
|
5155
5208
|
|
|
5156
|
-
|
|
5209
|
+
const EMOJI_BUTTON_SIZE = 44;
|
|
5210
|
+
|
|
5211
|
+
/** Renders a custom Discord emoji — only loads the image when visible */
|
|
5157
5212
|
function CustomEmoji({
|
|
5158
5213
|
discordId,
|
|
5159
5214
|
name,
|
|
5160
5215
|
animated,
|
|
5161
5216
|
selected,
|
|
5162
5217
|
size = 32,
|
|
5218
|
+
visible = true,
|
|
5163
5219
|
}: {
|
|
5164
5220
|
discordId: string;
|
|
5165
5221
|
name: string;
|
|
5166
5222
|
animated?: boolean;
|
|
5167
5223
|
selected?: boolean;
|
|
5168
5224
|
size?: number;
|
|
5225
|
+
visible?: boolean;
|
|
5169
5226
|
}) {
|
|
5170
5227
|
const [hovered, setHovered] = useState(false);
|
|
5228
|
+
|
|
5229
|
+
if (!visible) {
|
|
5230
|
+
return <div style={{ width: size, height: size }} />;
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5171
5233
|
const ext = animated && (hovered || selected) ? "gif" : "png";
|
|
5172
5234
|
const url = \`https://cdn.discordapp.com/emojis/\${discordId}.\${ext}?size=\${size}&quality=lossless\`;
|
|
5173
5235
|
|
|
@@ -5186,6 +5248,75 @@ function CustomEmoji({
|
|
|
5186
5248
|
);
|
|
5187
5249
|
}
|
|
5188
5250
|
|
|
5251
|
+
/** Wraps each emoji button and uses IntersectionObserver to only render content when in view */
|
|
5252
|
+
function LazyEmoji({
|
|
5253
|
+
emoji,
|
|
5254
|
+
selected,
|
|
5255
|
+
disabled,
|
|
5256
|
+
onClick,
|
|
5257
|
+
scrollRoot,
|
|
5258
|
+
}: {
|
|
5259
|
+
emoji: EmojiData;
|
|
5260
|
+
selected: boolean;
|
|
5261
|
+
disabled: boolean;
|
|
5262
|
+
onClick: () => void;
|
|
5263
|
+
scrollRoot: HTMLDivElement | null;
|
|
5264
|
+
}) {
|
|
5265
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
5266
|
+
const [visible, setVisible] = useState(false);
|
|
5267
|
+
|
|
5268
|
+
useEffect(() => {
|
|
5269
|
+
const el = ref.current;
|
|
5270
|
+
if (!el) return;
|
|
5271
|
+
|
|
5272
|
+
const observer = new IntersectionObserver(
|
|
5273
|
+
([entry]) => {
|
|
5274
|
+
if (entry.isIntersecting) {
|
|
5275
|
+
setVisible(true);
|
|
5276
|
+
observer.disconnect();
|
|
5277
|
+
}
|
|
5278
|
+
},
|
|
5279
|
+
{ root: scrollRoot, rootMargin: "100px" }
|
|
5280
|
+
);
|
|
5281
|
+
|
|
5282
|
+
observer.observe(el);
|
|
5283
|
+
return () => observer.disconnect();
|
|
5284
|
+
}, [scrollRoot]);
|
|
5285
|
+
|
|
5286
|
+
return (
|
|
5287
|
+
<button
|
|
5288
|
+
ref={ref}
|
|
5289
|
+
onClick={() => !disabled && onClick()}
|
|
5290
|
+
title={emoji.name}
|
|
5291
|
+
className={cn(
|
|
5292
|
+
"flex items-center justify-center rounded-md transition-colors",
|
|
5293
|
+
selected
|
|
5294
|
+
? "ring-2 ring-primary bg-accent/50"
|
|
5295
|
+
: "hover:bg-accent",
|
|
5296
|
+
disabled && "opacity-50 cursor-not-allowed"
|
|
5297
|
+
)}
|
|
5298
|
+
style={{ width: EMOJI_BUTTON_SIZE, height: EMOJI_BUTTON_SIZE }}
|
|
5299
|
+
>
|
|
5300
|
+
{emoji.source === "default" ? (
|
|
5301
|
+
visible ? (
|
|
5302
|
+
<span className="text-2xl leading-none">{emoji.id}</span>
|
|
5303
|
+
) : (
|
|
5304
|
+
<span style={{ width: 28, height: 28 }} />
|
|
5305
|
+
)
|
|
5306
|
+
) : (
|
|
5307
|
+
<CustomEmoji
|
|
5308
|
+
discordId={emoji.id}
|
|
5309
|
+
name={emoji.name}
|
|
5310
|
+
animated={emoji.animated}
|
|
5311
|
+
selected={selected}
|
|
5312
|
+
size={32}
|
|
5313
|
+
visible={visible}
|
|
5314
|
+
/>
|
|
5315
|
+
)}
|
|
5316
|
+
</button>
|
|
5317
|
+
);
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5189
5320
|
export function EmojiPicker({
|
|
5190
5321
|
guildDiscordId,
|
|
5191
5322
|
source = "all",
|
|
@@ -5194,18 +5325,17 @@ export function EmojiPicker({
|
|
|
5194
5325
|
value,
|
|
5195
5326
|
onSelect,
|
|
5196
5327
|
onChange,
|
|
5197
|
-
columns =
|
|
5198
|
-
pageSize = 64,
|
|
5328
|
+
columns = 9,
|
|
5199
5329
|
placeholder = "Pick emoji...",
|
|
5200
5330
|
className,
|
|
5201
5331
|
}: EmojiPickerProps) {
|
|
5202
5332
|
const [open, setOpen] = useState(false);
|
|
5203
5333
|
const [search, setSearch] = useState("");
|
|
5204
5334
|
const [selected, setSelected] = useState<EmojiData[]>([]);
|
|
5205
|
-
const [
|
|
5206
|
-
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
5335
|
+
const [activeSection, setActiveSection] = useState(0);
|
|
5207
5336
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
5208
5337
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
5338
|
+
const sectionRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
|
5209
5339
|
|
|
5210
5340
|
const currentSelected = value
|
|
5211
5341
|
? selected.filter((e) => value.includes(e.id))
|
|
@@ -5225,8 +5355,8 @@ export function EmojiPicker({
|
|
|
5225
5355
|
needsApp ? {} : "skip"
|
|
5226
5356
|
);
|
|
5227
5357
|
|
|
5228
|
-
// Build
|
|
5229
|
-
const
|
|
5358
|
+
// Build sections
|
|
5359
|
+
const allSections = useMemo(() => {
|
|
5230
5360
|
const sections: { header: string; items: EmojiData[] }[] = [];
|
|
5231
5361
|
|
|
5232
5362
|
if (needsGuild && guildEmojis && guildEmojis.length > 0) {
|
|
@@ -5271,42 +5401,35 @@ export function EmojiPicker({
|
|
|
5271
5401
|
|
|
5272
5402
|
// Filter by search
|
|
5273
5403
|
const filtered = useMemo(() => {
|
|
5274
|
-
if (!search) return
|
|
5404
|
+
if (!search) return allSections;
|
|
5275
5405
|
const q = search.toLowerCase();
|
|
5276
|
-
const flat =
|
|
5406
|
+
const flat = allSections.flatMap((s) => s.items).filter((e) =>
|
|
5277
5407
|
e.name.toLowerCase().includes(q)
|
|
5278
5408
|
);
|
|
5279
|
-
return flat.length > 0 ? [{ header: "", items: flat }] : [];
|
|
5280
|
-
}, [
|
|
5409
|
+
return flat.length > 0 ? [{ header: "Search Results", items: flat }] : [];
|
|
5410
|
+
}, [allSections, search]);
|
|
5281
5411
|
|
|
5282
|
-
//
|
|
5283
|
-
const totalCount = useMemo(
|
|
5284
|
-
() => filtered.reduce((acc, s) => acc + s.items.length, 0),
|
|
5285
|
-
[filtered]
|
|
5286
|
-
);
|
|
5287
|
-
|
|
5288
|
-
// Intersection observer for infinite scroll
|
|
5412
|
+
// Track which section is in view via IntersectionObserver
|
|
5289
5413
|
useEffect(() => {
|
|
5290
|
-
|
|
5291
|
-
|
|
5414
|
+
if (search) return;
|
|
5415
|
+
const container = scrollRef.current;
|
|
5416
|
+
if (!container) return;
|
|
5292
5417
|
|
|
5293
5418
|
const observer = new IntersectionObserver(
|
|
5294
|
-
(
|
|
5295
|
-
|
|
5296
|
-
|
|
5419
|
+
(entries) => {
|
|
5420
|
+
for (const entry of entries) {
|
|
5421
|
+
if (entry.isIntersecting) {
|
|
5422
|
+
const idx = Number(entry.target.getAttribute("data-section-index"));
|
|
5423
|
+
if (!isNaN(idx)) setActiveSection(idx);
|
|
5424
|
+
}
|
|
5297
5425
|
}
|
|
5298
5426
|
},
|
|
5299
|
-
{ root:
|
|
5427
|
+
{ root: container, rootMargin: "-10% 0px -80% 0px", threshold: 0 }
|
|
5300
5428
|
);
|
|
5301
5429
|
|
|
5302
|
-
observer.observe(
|
|
5430
|
+
sectionRefs.current.forEach((el) => observer.observe(el));
|
|
5303
5431
|
return () => observer.disconnect();
|
|
5304
|
-
}, [
|
|
5305
|
-
|
|
5306
|
-
// Reset visible count on search change
|
|
5307
|
-
useEffect(() => {
|
|
5308
|
-
setVisibleCount(pageSize);
|
|
5309
|
-
}, [search, pageSize]);
|
|
5432
|
+
}, [filtered, search]);
|
|
5310
5433
|
|
|
5311
5434
|
// Auto-focus search on open
|
|
5312
5435
|
useEffect(() => {
|
|
@@ -5314,9 +5437,17 @@ export function EmojiPicker({
|
|
|
5314
5437
|
setTimeout(() => searchRef.current?.focus(), 0);
|
|
5315
5438
|
} else {
|
|
5316
5439
|
setSearch("");
|
|
5440
|
+
setActiveSection(0);
|
|
5317
5441
|
}
|
|
5318
5442
|
}, [open]);
|
|
5319
5443
|
|
|
5444
|
+
const scrollToSection = useCallback((index: number) => {
|
|
5445
|
+
const el = sectionRefs.current.get(index);
|
|
5446
|
+
if (el) {
|
|
5447
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
5448
|
+
}
|
|
5449
|
+
}, []);
|
|
5450
|
+
|
|
5320
5451
|
const isSelected = useCallback(
|
|
5321
5452
|
(id: string) => {
|
|
5322
5453
|
if (value) return value.includes(id);
|
|
@@ -5335,7 +5466,6 @@ export function EmojiPicker({
|
|
|
5335
5466
|
return;
|
|
5336
5467
|
}
|
|
5337
5468
|
|
|
5338
|
-
// Multi mode
|
|
5339
5469
|
const alreadySelected = isSelected(emoji.id);
|
|
5340
5470
|
let next: EmojiData[];
|
|
5341
5471
|
|
|
@@ -5360,9 +5490,7 @@ export function EmojiPicker({
|
|
|
5360
5490
|
|
|
5361
5491
|
const atMax = mode === "multi" && maxCount !== undefined && currentSelected.length >= maxCount;
|
|
5362
5492
|
|
|
5363
|
-
|
|
5364
|
-
let rendered = 0;
|
|
5365
|
-
const gridStyle = { gridTemplateColumns: \`repeat(\${columns}, 1fr)\` };
|
|
5493
|
+
const gridStyle = { gridTemplateColumns: \`repeat(\${columns}, \${EMOJI_BUTTON_SIZE}px)\` };
|
|
5366
5494
|
|
|
5367
5495
|
// Trigger display
|
|
5368
5496
|
const renderTrigger = () => {
|
|
@@ -5384,7 +5512,6 @@ export function EmojiPicker({
|
|
|
5384
5512
|
);
|
|
5385
5513
|
}
|
|
5386
5514
|
|
|
5387
|
-
// Multi mode
|
|
5388
5515
|
const shown = currentSelected.slice(0, 5);
|
|
5389
5516
|
const extra = currentSelected.length - shown.length;
|
|
5390
5517
|
return (
|
|
@@ -5425,125 +5552,138 @@ export function EmojiPicker({
|
|
|
5425
5552
|
<span className="ml-2 text-lg opacity-50">\uD83D\uDE00</span>
|
|
5426
5553
|
</Button>
|
|
5427
5554
|
</PopoverTrigger>
|
|
5428
|
-
<PopoverContent className="w-
|
|
5429
|
-
{
|
|
5430
|
-
|
|
5431
|
-
<
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
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"
|
|
5555
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
5556
|
+
<div className="flex flex-col" style={{ width: columns * EMOJI_BUTTON_SIZE + 52 + 16 }}>
|
|
5557
|
+
{/* Search bar */}
|
|
5558
|
+
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
5559
|
+
<svg
|
|
5560
|
+
className="h-4 w-4 shrink-0 opacity-50"
|
|
5561
|
+
fill="none"
|
|
5562
|
+
stroke="currentColor"
|
|
5563
|
+
viewBox="0 0 24 24"
|
|
5456
5564
|
>
|
|
5457
|
-
<
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5565
|
+
<path
|
|
5566
|
+
strokeLinecap="round"
|
|
5567
|
+
strokeLinejoin="round"
|
|
5568
|
+
strokeWidth={2}
|
|
5569
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
5570
|
+
/>
|
|
5571
|
+
</svg>
|
|
5572
|
+
<input
|
|
5573
|
+
ref={searchRef}
|
|
5574
|
+
type="text"
|
|
5575
|
+
placeholder="Search emojis..."
|
|
5576
|
+
value={search}
|
|
5577
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
5578
|
+
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
5579
|
+
/>
|
|
5580
|
+
{search && (
|
|
5581
|
+
<button
|
|
5582
|
+
onClick={() => setSearch("")}
|
|
5583
|
+
className="text-muted-foreground hover:text-foreground"
|
|
5584
|
+
>
|
|
5585
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
5586
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
5587
|
+
</svg>
|
|
5588
|
+
</button>
|
|
5589
|
+
)}
|
|
5590
|
+
</div>
|
|
5463
5591
|
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5592
|
+
{/* Main body: sidebar + emoji grid */}
|
|
5593
|
+
<div className="flex">
|
|
5594
|
+
{/* Category sidebar */}
|
|
5595
|
+
{!search && (
|
|
5596
|
+
<div className="flex flex-col items-center gap-0.5 border-r py-1.5 px-1 overflow-y-auto" style={{ maxHeight: 420 }}>
|
|
5597
|
+
{filtered.map((section, i) => (
|
|
5598
|
+
<button
|
|
5599
|
+
key={section.header}
|
|
5600
|
+
onClick={() => scrollToSection(i)}
|
|
5601
|
+
title={section.header}
|
|
5602
|
+
className={cn(
|
|
5603
|
+
"flex items-center justify-center rounded-md w-8 h-8 shrink-0 transition-colors",
|
|
5604
|
+
activeSection === i
|
|
5605
|
+
? "bg-accent text-accent-foreground"
|
|
5606
|
+
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
|
|
5607
|
+
)}
|
|
5608
|
+
>
|
|
5609
|
+
{CATEGORY_ICONS[section.header] ?? (
|
|
5610
|
+
<span className="text-sm leading-none">
|
|
5611
|
+
{section.items[0]?.source === "default"
|
|
5612
|
+
? section.items[0].id
|
|
5613
|
+
: "?"}
|
|
5614
|
+
</span>
|
|
5615
|
+
)}
|
|
5616
|
+
</button>
|
|
5617
|
+
))}
|
|
5618
|
+
</div>
|
|
5619
|
+
)}
|
|
5474
5620
|
|
|
5475
|
-
|
|
5476
|
-
|
|
5621
|
+
{/* Scrollable emoji grid */}
|
|
5622
|
+
<div
|
|
5623
|
+
ref={scrollRef}
|
|
5624
|
+
className="overflow-y-auto p-2 flex-1"
|
|
5625
|
+
style={{ maxHeight: 420 }}
|
|
5626
|
+
>
|
|
5627
|
+
{filtered.length === 0 && (
|
|
5628
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
5629
|
+
No emojis found
|
|
5630
|
+
</div>
|
|
5631
|
+
)}
|
|
5477
5632
|
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5633
|
+
{filtered.map((section, sectionIdx) => (
|
|
5634
|
+
<div
|
|
5635
|
+
key={section.header || "search-results"}
|
|
5636
|
+
ref={(el) => {
|
|
5637
|
+
if (el) sectionRefs.current.set(sectionIdx, el);
|
|
5638
|
+
else sectionRefs.current.delete(sectionIdx);
|
|
5639
|
+
}}
|
|
5640
|
+
data-section-index={sectionIdx}
|
|
5641
|
+
>
|
|
5642
|
+
{section.header && (
|
|
5643
|
+
<div className="sticky top-0 z-10 bg-popover px-1 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
5644
|
+
{section.header}
|
|
5645
|
+
</div>
|
|
5646
|
+
)}
|
|
5647
|
+
<div className="grid" style={gridStyle}>
|
|
5648
|
+
{section.items.map((emoji) => {
|
|
5649
|
+
const sel = isSelected(emoji.id);
|
|
5650
|
+
const disabled = !sel && atMax;
|
|
5481
5651
|
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5652
|
+
return (
|
|
5653
|
+
<LazyEmoji
|
|
5654
|
+
key={\`\${emoji.source}-\${emoji.id}\`}
|
|
5655
|
+
emoji={emoji}
|
|
5656
|
+
selected={sel}
|
|
5657
|
+
disabled={disabled}
|
|
5658
|
+
onClick={() => handleSelect(emoji)}
|
|
5659
|
+
scrollRoot={scrollRef.current}
|
|
5660
|
+
/>
|
|
5661
|
+
);
|
|
5662
|
+
})}
|
|
5487
5663
|
</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
5664
|
</div>
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5665
|
+
))}
|
|
5666
|
+
</div>
|
|
5667
|
+
</div>
|
|
5525
5668
|
|
|
5526
|
-
{/*
|
|
5527
|
-
|
|
5669
|
+
{/* Multi-select footer */}
|
|
5670
|
+
{mode === "multi" && (
|
|
5671
|
+
<div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
|
|
5672
|
+
<span>
|
|
5673
|
+
Selected: {currentSelected.length}
|
|
5674
|
+
{maxCount !== undefined && \` / \${maxCount}\`}
|
|
5675
|
+
</span>
|
|
5676
|
+
{currentSelected.length > 0 && (
|
|
5677
|
+
<button
|
|
5678
|
+
onClick={handleClear}
|
|
5679
|
+
className="text-xs text-muted-foreground hover:text-foreground underline"
|
|
5680
|
+
>
|
|
5681
|
+
Clear
|
|
5682
|
+
</button>
|
|
5683
|
+
)}
|
|
5684
|
+
</div>
|
|
5685
|
+
)}
|
|
5528
5686
|
</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
5687
|
</PopoverContent>
|
|
5548
5688
|
</Popover>
|
|
5549
5689
|
);
|