Live video on the AT Protocol

frimousse categories

+116 -2
+116 -2
js/app/components/emoji-picker/emoji-picker.web.tsx
··· 4 4 SkinTone, 5 5 useSkinTone, 6 6 } from "frimousse"; 7 - import { ChevronUp } from "lucide-react-native"; 8 - import React, { useEffect, useMemo, useRef, useState } from "react"; 7 + import { 8 + ChevronUp, 9 + Flag, 10 + Hash, 11 + Lightbulb, 12 + LucideIcon, 13 + PawPrint, 14 + PersonStanding, 15 + Pizza, 16 + Plane, 17 + Smile, 18 + Volleyball, 19 + } from "lucide-react-native"; 20 + import React, { 21 + useCallback, 22 + useEffect, 23 + useMemo, 24 + useRef, 25 + useState, 26 + } from "react"; 9 27 import { View } from "react-native"; 10 28 import { useEmojiData } from "utils/emoji"; 29 + 30 + const CATEGORY_ICONS: { label: string; Icon: LucideIcon }[] = [ 31 + { label: "Smileys & Emotion", Icon: Smile }, 32 + { label: "People & Body", Icon: PersonStanding }, 33 + { label: "Animals & Nature", Icon: PawPrint }, 34 + { label: "Food & Drink", Icon: Pizza }, 35 + { label: "Travel & Places", Icon: Plane }, 36 + { label: "Activities", Icon: Volleyball }, 37 + { label: "Objects", Icon: Lightbulb }, 38 + { label: "Symbols", Icon: Hash }, 39 + { label: "Flags", Icon: Flag }, 40 + ]; 11 41 12 42 export type SelectedEmoji = 13 43 | { type: "standard"; native: string } ··· 139 169 const { theme } = useTheme(); 140 170 const emojiData = useEmojiData(); 141 171 const [skinToneOpen, setSkinToneOpen] = useState(false); 172 + const [activeCategory, setActiveCategory] = useState(0); 173 + const viewportRef = useRef<HTMLDivElement>(null); 142 174 143 175 const nativeToId = useMemo(() => { 144 176 if (!emojiData) return null; ··· 169 201 }; 170 202 }, [isOpen, onClose]); 171 203 204 + useEffect(() => { 205 + const viewport = viewportRef.current; 206 + if (!viewport) return; 207 + 208 + const handleScroll = () => { 209 + const sizer = viewport.querySelector<HTMLElement>( 210 + "[frimousse-list-sizer]", 211 + ); 212 + // Skip index 0 — it's the hidden measurement element frimousse renders 213 + const categories = Array.from( 214 + viewport.querySelectorAll<HTMLElement>("[frimousse-category]"), 215 + ).slice(1); 216 + const sizerOffset = sizer?.offsetTop ?? 0; 217 + const scrollTop = viewport.scrollTop; 218 + let active = 0; 219 + for (let i = 0; i < categories.length; i++) { 220 + if (sizerOffset + categories[i].offsetTop <= scrollTop + 8) active = i; 221 + } 222 + setActiveCategory(active); 223 + }; 224 + 225 + viewport.addEventListener("scroll", handleScroll, { passive: true }); 226 + return () => viewport.removeEventListener("scroll", handleScroll); 227 + }, [isOpen]); 228 + 229 + const scrollToCategory = useCallback((index: number) => { 230 + const viewport = viewportRef.current; 231 + if (!viewport) return; 232 + const sizer = viewport.querySelector<HTMLElement>("[frimousse-list-sizer]"); 233 + // Skip index 0 — it's the hidden measurement element frimousse renders 234 + const categories = Array.from( 235 + viewport.querySelectorAll<HTMLElement>("[frimousse-category]"), 236 + ).slice(1); 237 + const category = categories[index]; 238 + const sizerOffset = sizer?.offsetTop ?? 0; 239 + if (category) { 240 + viewport.scrollTo({ 241 + top: sizerOffset + category.offsetTop, 242 + behavior: "smooth", 243 + }); 244 + setActiveCategory(index); 245 + } 246 + }, []); 247 + 172 248 if (!isOpen) return null; 173 249 const handleStandardSelect = (arg: any) => { 174 250 onSelect?.({ type: "standard", native: arg.emoji ?? arg }); ··· 288 364 placeholder="Search emoji…" 289 365 autoFocus 290 366 /> 367 + <div 368 + style={{ 369 + display: "flex", 370 + justifyContent: "space-around", 371 + padding: "4px 14px", 372 + borderBottom: `1px solid ${theme.colors.border}`, 373 + }} 374 + > 375 + {CATEGORY_ICONS.map(({ label, Icon }, i) => ( 376 + <button 377 + key={label} 378 + title={label} 379 + onClick={() => scrollToCategory(i)} 380 + style={{ 381 + width: 30, 382 + height: 30, 383 + borderRadius: 6, 384 + border: "none", 385 + background: 386 + activeCategory === i 387 + ? "rgba(255,255,255,0.12)" 388 + : "transparent", 389 + cursor: "pointer", 390 + display: "flex", 391 + alignItems: "center", 392 + justifyContent: "center", 393 + color: 394 + activeCategory === i 395 + ? "rgba(255,255,255,0.9)" 396 + : "rgba(255,255,255,0.4)", 397 + transition: "color 0.15s ease, background 0.15s ease", 398 + }} 399 + > 400 + <Icon size={16} /> 401 + </button> 402 + ))} 403 + </div> 291 404 <FrimousseEmojiPicker.Viewport 405 + ref={viewportRef} 292 406 style={{ flex: 1, position: "relative" }} 293 407 > 294 408 <FrimousseEmojiPicker.Loading