tangled
alpha
login
or
join now
stream.place
/
streamplace
74
fork
atom
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
frimousse categories
Natalie B.
2 weeks ago
71a8cf68
402e8ca2
+116
-2
1 changed file
expand all
collapse all
unified
split
js
app
components
emoji-picker
emoji-picker.web.tsx
+116
-2
js/app/components/emoji-picker/emoji-picker.web.tsx
···
4
4
SkinTone,
5
5
useSkinTone,
6
6
} from "frimousse";
7
7
-
import { ChevronUp } from "lucide-react-native";
8
8
-
import React, { useEffect, useMemo, useRef, useState } from "react";
7
7
+
import {
8
8
+
ChevronUp,
9
9
+
Flag,
10
10
+
Hash,
11
11
+
Lightbulb,
12
12
+
LucideIcon,
13
13
+
PawPrint,
14
14
+
PersonStanding,
15
15
+
Pizza,
16
16
+
Plane,
17
17
+
Smile,
18
18
+
Volleyball,
19
19
+
} from "lucide-react-native";
20
20
+
import React, {
21
21
+
useCallback,
22
22
+
useEffect,
23
23
+
useMemo,
24
24
+
useRef,
25
25
+
useState,
26
26
+
} from "react";
9
27
import { View } from "react-native";
10
28
import { useEmojiData } from "utils/emoji";
29
29
+
30
30
+
const CATEGORY_ICONS: { label: string; Icon: LucideIcon }[] = [
31
31
+
{ label: "Smileys & Emotion", Icon: Smile },
32
32
+
{ label: "People & Body", Icon: PersonStanding },
33
33
+
{ label: "Animals & Nature", Icon: PawPrint },
34
34
+
{ label: "Food & Drink", Icon: Pizza },
35
35
+
{ label: "Travel & Places", Icon: Plane },
36
36
+
{ label: "Activities", Icon: Volleyball },
37
37
+
{ label: "Objects", Icon: Lightbulb },
38
38
+
{ label: "Symbols", Icon: Hash },
39
39
+
{ label: "Flags", Icon: Flag },
40
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
172
+
const [activeCategory, setActiveCategory] = useState(0);
173
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
204
+
useEffect(() => {
205
205
+
const viewport = viewportRef.current;
206
206
+
if (!viewport) return;
207
207
+
208
208
+
const handleScroll = () => {
209
209
+
const sizer = viewport.querySelector<HTMLElement>(
210
210
+
"[frimousse-list-sizer]",
211
211
+
);
212
212
+
// Skip index 0 — it's the hidden measurement element frimousse renders
213
213
+
const categories = Array.from(
214
214
+
viewport.querySelectorAll<HTMLElement>("[frimousse-category]"),
215
215
+
).slice(1);
216
216
+
const sizerOffset = sizer?.offsetTop ?? 0;
217
217
+
const scrollTop = viewport.scrollTop;
218
218
+
let active = 0;
219
219
+
for (let i = 0; i < categories.length; i++) {
220
220
+
if (sizerOffset + categories[i].offsetTop <= scrollTop + 8) active = i;
221
221
+
}
222
222
+
setActiveCategory(active);
223
223
+
};
224
224
+
225
225
+
viewport.addEventListener("scroll", handleScroll, { passive: true });
226
226
+
return () => viewport.removeEventListener("scroll", handleScroll);
227
227
+
}, [isOpen]);
228
228
+
229
229
+
const scrollToCategory = useCallback((index: number) => {
230
230
+
const viewport = viewportRef.current;
231
231
+
if (!viewport) return;
232
232
+
const sizer = viewport.querySelector<HTMLElement>("[frimousse-list-sizer]");
233
233
+
// Skip index 0 — it's the hidden measurement element frimousse renders
234
234
+
const categories = Array.from(
235
235
+
viewport.querySelectorAll<HTMLElement>("[frimousse-category]"),
236
236
+
).slice(1);
237
237
+
const category = categories[index];
238
238
+
const sizerOffset = sizer?.offsetTop ?? 0;
239
239
+
if (category) {
240
240
+
viewport.scrollTo({
241
241
+
top: sizerOffset + category.offsetTop,
242
242
+
behavior: "smooth",
243
243
+
});
244
244
+
setActiveCategory(index);
245
245
+
}
246
246
+
}, []);
247
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
367
+
<div
368
368
+
style={{
369
369
+
display: "flex",
370
370
+
justifyContent: "space-around",
371
371
+
padding: "4px 14px",
372
372
+
borderBottom: `1px solid ${theme.colors.border}`,
373
373
+
}}
374
374
+
>
375
375
+
{CATEGORY_ICONS.map(({ label, Icon }, i) => (
376
376
+
<button
377
377
+
key={label}
378
378
+
title={label}
379
379
+
onClick={() => scrollToCategory(i)}
380
380
+
style={{
381
381
+
width: 30,
382
382
+
height: 30,
383
383
+
borderRadius: 6,
384
384
+
border: "none",
385
385
+
background:
386
386
+
activeCategory === i
387
387
+
? "rgba(255,255,255,0.12)"
388
388
+
: "transparent",
389
389
+
cursor: "pointer",
390
390
+
display: "flex",
391
391
+
alignItems: "center",
392
392
+
justifyContent: "center",
393
393
+
color:
394
394
+
activeCategory === i
395
395
+
? "rgba(255,255,255,0.9)"
396
396
+
: "rgba(255,255,255,0.4)",
397
397
+
transition: "color 0.15s ease, background 0.15s ease",
398
398
+
}}
399
399
+
>
400
400
+
<Icon size={16} />
401
401
+
</button>
402
402
+
))}
403
403
+
</div>
291
404
<FrimousseEmojiPicker.Viewport
405
405
+
ref={viewportRef}
292
406
style={{ flex: 1, position: "relative" }}
293
407
>
294
408
<FrimousseEmojiPicker.Loading