@refraction-ui/astro 0.16.1 → 0.17.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.
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Emoji data — the single source of truth for the whole library.
3
+ *
4
+ * The full base emoji set (Unicode 16.0, ~1900 emoji) is generated offline into
5
+ * `emoji-dataset.generated.ts` from the canonical `scripts/emoji/emoji-test-16.0.txt`
6
+ * (regenerate with `node scripts/generate-emoji-data.mjs`). This module decodes
7
+ * that compact data and layers on curated `:shortcode:` aliases, search, and
8
+ * resolution helpers. It is consumed by:
9
+ * - the emoji picker (grid + category tabs + search + recents),
10
+ * - the composer's `:` shortcode trigger (`createEmojiTrigger`),
11
+ * - rich-editor's `EMOJI_MAP` (`:shortcode:` → glyph).
12
+ * so the three no longer diverge.
13
+ */
14
+ import {
15
+ CATEGORY_ORDER,
16
+ EMOJI_TUPLES,
17
+ EMOJI_COUNT,
18
+ } from './emoji-dataset.generated.js'
19
+
1
20
  export type EmojiCategory =
2
21
  | 'smileys'
3
22
  | 'people'
@@ -10,260 +29,36 @@ export type EmojiCategory =
10
29
  | 'flags'
11
30
 
12
31
  export interface EmojiEntry {
32
+ /** The rendered glyph (native codepoints). */
13
33
  emoji: string
34
+ /** CLDR short name, e.g. "face with tears of joy". */
14
35
  name: string
15
36
  category: EmojiCategory
37
+ /** Search keywords (does not include the name words verbatim-repeated). */
16
38
  keywords: string[]
39
+ /** Canonical `snake_case` shortcode body (no surrounding colons). */
40
+ shortcode: string
17
41
  }
18
42
 
19
- export const EMOJI_DATA: Record<EmojiCategory, EmojiEntry[]> = {
20
- smileys: [
21
- { emoji: '\u{1F600}', name: 'grinning face', category: 'smileys', keywords: ['happy', 'smile', 'grin'] },
22
- { emoji: '\u{1F603}', name: 'grinning face with big eyes', category: 'smileys', keywords: ['happy', 'smile'] },
23
- { emoji: '\u{1F604}', name: 'grinning face with smiling eyes', category: 'smileys', keywords: ['happy', 'joy'] },
24
- { emoji: '\u{1F601}', name: 'beaming face', category: 'smileys', keywords: ['happy', 'grin', 'beam'] },
25
- { emoji: '\u{1F606}', name: 'grinning squinting face', category: 'smileys', keywords: ['laugh', 'happy'] },
26
- { emoji: '\u{1F605}', name: 'grinning face with sweat', category: 'smileys', keywords: ['nervous', 'laugh'] },
27
- { emoji: '\u{1F923}', name: 'rolling on the floor laughing', category: 'smileys', keywords: ['lol', 'rofl', 'laugh'] },
28
- { emoji: '\u{1F602}', name: 'face with tears of joy', category: 'smileys', keywords: ['laugh', 'cry', 'joy', 'lol'] },
29
- { emoji: '\u{1F642}', name: 'slightly smiling face', category: 'smileys', keywords: ['smile', 'happy'] },
30
- { emoji: '\u{1F643}', name: 'upside-down face', category: 'smileys', keywords: ['silly', 'sarcasm'] },
31
- { emoji: '\u{1F609}', name: 'winking face', category: 'smileys', keywords: ['wink', 'flirt'] },
32
- { emoji: '\u{1F60A}', name: 'smiling face with smiling eyes', category: 'smileys', keywords: ['blush', 'happy'] },
33
- { emoji: '\u{1F607}', name: 'smiling face with halo', category: 'smileys', keywords: ['angel', 'innocent'] },
34
- { emoji: '\u{1F970}', name: 'smiling face with hearts', category: 'smileys', keywords: ['love', 'crush', 'adore'] },
35
- { emoji: '\u{1F60D}', name: 'smiling face with heart-eyes', category: 'smileys', keywords: ['love', 'heart'] },
36
- { emoji: '\u{1F929}', name: 'star-struck', category: 'smileys', keywords: ['star', 'eyes', 'wow'] },
37
- { emoji: '\u{1F618}', name: 'face blowing a kiss', category: 'smileys', keywords: ['kiss', 'love'] },
38
- { emoji: '\u{1F617}', name: 'kissing face', category: 'smileys', keywords: ['kiss'] },
39
- { emoji: '\u{1F61A}', name: 'kissing face with closed eyes', category: 'smileys', keywords: ['kiss', 'love'] },
40
- { emoji: '\u{1F619}', name: 'kissing face with smiling eyes', category: 'smileys', keywords: ['kiss'] },
41
- { emoji: '\u{1F60B}', name: 'face savoring food', category: 'smileys', keywords: ['yummy', 'delicious'] },
42
- { emoji: '\u{1F61C}', name: 'winking face with tongue', category: 'smileys', keywords: ['tongue', 'wink', 'silly'] },
43
- { emoji: '\u{1F61D}', name: 'squinting face with tongue', category: 'smileys', keywords: ['tongue', 'silly'] },
44
- { emoji: '\u{1F911}', name: 'money-mouth face', category: 'smileys', keywords: ['money', 'rich'] },
45
- { emoji: '\u{1F917}', name: 'hugging face', category: 'smileys', keywords: ['hug', 'warm'] },
46
- { emoji: '\u{1F914}', name: 'thinking face', category: 'smileys', keywords: ['think', 'hmm'] },
47
- { emoji: '\u{1F910}', name: 'zipper-mouth face', category: 'smileys', keywords: ['secret', 'quiet', 'zip'] },
48
- { emoji: '\u{1F928}', name: 'face with raised eyebrow', category: 'smileys', keywords: ['skeptical', 'doubt'] },
49
- { emoji: '\u{1F610}', name: 'neutral face', category: 'smileys', keywords: ['meh', 'neutral'] },
50
- { emoji: '\u{1F611}', name: 'expressionless face', category: 'smileys', keywords: ['blank', 'expressionless'] },
51
- ],
52
- people: [
53
- { emoji: '\u{1F44B}', name: 'waving hand', category: 'people', keywords: ['wave', 'hello', 'hi', 'bye'] },
54
- { emoji: '\u{1F91A}', name: 'raised back of hand', category: 'people', keywords: ['hand', 'backhand'] },
55
- { emoji: '\u{1F590}', name: 'hand with fingers splayed', category: 'people', keywords: ['hand', 'fingers'] },
56
- { emoji: '\u270B', name: 'raised hand', category: 'people', keywords: ['hand', 'stop', 'high five'] },
57
- { emoji: '\u{1F596}', name: 'vulcan salute', category: 'people', keywords: ['spock', 'star trek'] },
58
- { emoji: '\u{1F44C}', name: 'OK hand', category: 'people', keywords: ['ok', 'perfect', 'fine'] },
59
- { emoji: '\u270C', name: 'victory hand', category: 'people', keywords: ['peace', 'victory', 'v'] },
60
- { emoji: '\u{1F91E}', name: 'crossed fingers', category: 'people', keywords: ['luck', 'hope'] },
61
- { emoji: '\u{1F918}', name: 'love-you gesture', category: 'people', keywords: ['love', 'rock'] },
62
- { emoji: '\u{1F919}', name: 'call me hand', category: 'people', keywords: ['call', 'phone'] },
63
- { emoji: '\u{1F448}', name: 'backhand index pointing left', category: 'people', keywords: ['left', 'point'] },
64
- { emoji: '\u{1F449}', name: 'backhand index pointing right', category: 'people', keywords: ['right', 'point'] },
65
- { emoji: '\u{1F446}', name: 'backhand index pointing up', category: 'people', keywords: ['up', 'point'] },
66
- { emoji: '\u{1F447}', name: 'backhand index pointing down', category: 'people', keywords: ['down', 'point'] },
67
- { emoji: '\u261D', name: 'index pointing up', category: 'people', keywords: ['up', 'point'] },
68
- { emoji: '\u{1F44D}', name: 'thumbs up', category: 'people', keywords: ['like', 'yes', 'approve', 'thumbsup'] },
69
- { emoji: '\u{1F44E}', name: 'thumbs down', category: 'people', keywords: ['dislike', 'no', 'disapprove'] },
70
- { emoji: '\u270A', name: 'raised fist', category: 'people', keywords: ['fist', 'power'] },
71
- { emoji: '\u{1F44A}', name: 'oncoming fist', category: 'people', keywords: ['punch', 'fist bump'] },
72
- { emoji: '\u{1F91B}', name: 'left-facing fist', category: 'people', keywords: ['fist'] },
73
- { emoji: '\u{1F91C}', name: 'right-facing fist', category: 'people', keywords: ['fist'] },
74
- { emoji: '\u{1F44F}', name: 'clapping hands', category: 'people', keywords: ['clap', 'applause', 'bravo'] },
75
- { emoji: '\u{1F64C}', name: 'raising hands', category: 'people', keywords: ['celebrate', 'hooray'] },
76
- { emoji: '\u{1F450}', name: 'open hands', category: 'people', keywords: ['hands', 'open'] },
77
- { emoji: '\u{1F932}', name: 'palms up together', category: 'people', keywords: ['prayer', 'please'] },
78
- { emoji: '\u{1F91D}', name: 'handshake', category: 'people', keywords: ['deal', 'agree', 'shake'] },
79
- { emoji: '\u{1F64F}', name: 'folded hands', category: 'people', keywords: ['pray', 'please', 'thank you'] },
80
- ],
81
- nature: [
82
- { emoji: '\u{1F436}', name: 'dog face', category: 'nature', keywords: ['dog', 'puppy', 'pet'] },
83
- { emoji: '\u{1F431}', name: 'cat face', category: 'nature', keywords: ['cat', 'kitten', 'pet'] },
84
- { emoji: '\u{1F42D}', name: 'mouse face', category: 'nature', keywords: ['mouse', 'rodent'] },
85
- { emoji: '\u{1F439}', name: 'hamster', category: 'nature', keywords: ['hamster', 'pet'] },
86
- { emoji: '\u{1F430}', name: 'rabbit face', category: 'nature', keywords: ['rabbit', 'bunny'] },
87
- { emoji: '\u{1F98A}', name: 'fox', category: 'nature', keywords: ['fox', 'animal'] },
88
- { emoji: '\u{1F43B}', name: 'bear', category: 'nature', keywords: ['bear', 'animal'] },
89
- { emoji: '\u{1F43C}', name: 'panda', category: 'nature', keywords: ['panda', 'animal'] },
90
- { emoji: '\u{1F428}', name: 'koala', category: 'nature', keywords: ['koala', 'animal'] },
91
- { emoji: '\u{1F42F}', name: 'tiger face', category: 'nature', keywords: ['tiger', 'animal'] },
92
- { emoji: '\u{1F981}', name: 'lion', category: 'nature', keywords: ['lion', 'animal'] },
93
- { emoji: '\u{1F42E}', name: 'cow face', category: 'nature', keywords: ['cow', 'animal'] },
94
- { emoji: '\u{1F437}', name: 'pig face', category: 'nature', keywords: ['pig', 'animal'] },
95
- { emoji: '\u{1F438}', name: 'frog', category: 'nature', keywords: ['frog', 'animal'] },
96
- { emoji: '\u{1F435}', name: 'monkey face', category: 'nature', keywords: ['monkey', 'animal'] },
97
- { emoji: '\u{1F412}', name: 'monkey', category: 'nature', keywords: ['monkey', 'animal'] },
98
- { emoji: '\u{1F414}', name: 'chicken', category: 'nature', keywords: ['chicken', 'animal'] },
99
- { emoji: '\u{1F427}', name: 'penguin', category: 'nature', keywords: ['penguin', 'animal'] },
100
- { emoji: '\u{1F426}', name: 'bird', category: 'nature', keywords: ['bird', 'animal'] },
101
- { emoji: '\u{1F40A}', name: 'crocodile', category: 'nature', keywords: ['crocodile', 'animal'] },
102
- { emoji: '\u{1F422}', name: 'turtle', category: 'nature', keywords: ['turtle', 'slow'] },
103
- { emoji: '\u{1F40D}', name: 'snake', category: 'nature', keywords: ['snake', 'animal'] },
104
- { emoji: '\u{1F332}', name: 'evergreen tree', category: 'nature', keywords: ['tree', 'pine'] },
105
- { emoji: '\u{1F333}', name: 'deciduous tree', category: 'nature', keywords: ['tree'] },
106
- { emoji: '\u{1F334}', name: 'palm tree', category: 'nature', keywords: ['palm', 'tree', 'tropical'] },
107
- { emoji: '\u{1F335}', name: 'cactus', category: 'nature', keywords: ['cactus', 'desert'] },
108
- { emoji: '\u{1F337}', name: 'tulip', category: 'nature', keywords: ['flower', 'tulip'] },
109
- { emoji: '\u{1F339}', name: 'rose', category: 'nature', keywords: ['flower', 'rose', 'love'] },
110
- ],
111
- food: [
112
- { emoji: '\u{1F34E}', name: 'red apple', category: 'food', keywords: ['apple', 'fruit'] },
113
- { emoji: '\u{1F34A}', name: 'tangerine', category: 'food', keywords: ['orange', 'fruit'] },
114
- { emoji: '\u{1F34B}', name: 'lemon', category: 'food', keywords: ['lemon', 'fruit'] },
115
- { emoji: '\u{1F34C}', name: 'banana', category: 'food', keywords: ['banana', 'fruit'] },
116
- { emoji: '\u{1F349}', name: 'watermelon', category: 'food', keywords: ['watermelon', 'fruit'] },
117
- { emoji: '\u{1F347}', name: 'grapes', category: 'food', keywords: ['grapes', 'fruit'] },
118
- { emoji: '\u{1F353}', name: 'strawberry', category: 'food', keywords: ['strawberry', 'fruit'] },
119
- { emoji: '\u{1F352}', name: 'cherries', category: 'food', keywords: ['cherry', 'fruit'] },
120
- { emoji: '\u{1F351}', name: 'peach', category: 'food', keywords: ['peach', 'fruit'] },
121
- { emoji: '\u{1F34D}', name: 'pineapple', category: 'food', keywords: ['pineapple', 'fruit'] },
122
- { emoji: '\u{1F354}', name: 'hamburger', category: 'food', keywords: ['burger', 'fast food'] },
123
- { emoji: '\u{1F355}', name: 'pizza', category: 'food', keywords: ['pizza', 'food'] },
124
- { emoji: '\u{1F32E}', name: 'taco', category: 'food', keywords: ['taco', 'mexican'] },
125
- { emoji: '\u{1F32F}', name: 'burrito', category: 'food', keywords: ['burrito', 'mexican'] },
126
- { emoji: '\u{1F35F}', name: 'french fries', category: 'food', keywords: ['fries', 'fast food'] },
127
- { emoji: '\u{1F357}', name: 'poultry leg', category: 'food', keywords: ['chicken', 'meat'] },
128
- { emoji: '\u{1F356}', name: 'meat on bone', category: 'food', keywords: ['meat', 'bone'] },
129
- { emoji: '\u{1F363}', name: 'sushi', category: 'food', keywords: ['sushi', 'japanese'] },
130
- { emoji: '\u{1F370}', name: 'shortcake', category: 'food', keywords: ['cake', 'dessert'] },
131
- { emoji: '\u{1F36B}', name: 'chocolate bar', category: 'food', keywords: ['chocolate', 'candy'] },
132
- { emoji: '\u{1F369}', name: 'doughnut', category: 'food', keywords: ['donut', 'dessert'] },
133
- { emoji: '\u{1F36A}', name: 'cookie', category: 'food', keywords: ['cookie', 'dessert'] },
134
- { emoji: '\u2615', name: 'hot beverage', category: 'food', keywords: ['coffee', 'tea', 'drink'] },
135
- { emoji: '\u{1F37A}', name: 'beer mug', category: 'food', keywords: ['beer', 'drink'] },
136
- { emoji: '\u{1F377}', name: 'wine glass', category: 'food', keywords: ['wine', 'drink'] },
137
- ],
138
- travel: [
139
- { emoji: '\u{1F697}', name: 'automobile', category: 'travel', keywords: ['car', 'drive'] },
140
- { emoji: '\u{1F695}', name: 'taxi', category: 'travel', keywords: ['taxi', 'cab'] },
141
- { emoji: '\u{1F68C}', name: 'bus', category: 'travel', keywords: ['bus', 'transit'] },
142
- { emoji: '\u{1F682}', name: 'locomotive', category: 'travel', keywords: ['train', 'rail'] },
143
- { emoji: '\u2708', name: 'airplane', category: 'travel', keywords: ['plane', 'fly', 'travel'] },
144
- { emoji: '\u{1F680}', name: 'rocket', category: 'travel', keywords: ['rocket', 'space', 'launch'] },
145
- { emoji: '\u{1F6F8}', name: 'flying saucer', category: 'travel', keywords: ['ufo', 'alien'] },
146
- { emoji: '\u{1F6A2}', name: 'ship', category: 'travel', keywords: ['ship', 'boat', 'cruise'] },
147
- { emoji: '\u{1F3E0}', name: 'house', category: 'travel', keywords: ['house', 'home'] },
148
- { emoji: '\u{1F3E2}', name: 'office building', category: 'travel', keywords: ['office', 'building', 'work'] },
149
- { emoji: '\u{1F3EB}', name: 'school', category: 'travel', keywords: ['school', 'education'] },
150
- { emoji: '\u{1F3E5}', name: 'hospital', category: 'travel', keywords: ['hospital', 'health'] },
151
- { emoji: '\u{1F30D}', name: 'globe showing Europe-Africa', category: 'travel', keywords: ['earth', 'globe', 'world'] },
152
- { emoji: '\u{1F30E}', name: 'globe showing Americas', category: 'travel', keywords: ['earth', 'globe', 'world'] },
153
- { emoji: '\u{1F30F}', name: 'globe showing Asia-Australia', category: 'travel', keywords: ['earth', 'globe', 'world'] },
154
- { emoji: '\u{1F5FC}', name: 'Tokyo tower', category: 'travel', keywords: ['tokyo', 'japan', 'tower'] },
155
- { emoji: '\u{1F5FD}', name: 'Statue of Liberty', category: 'travel', keywords: ['statue', 'liberty', 'new york'] },
156
- { emoji: '\u26F0', name: 'mountain', category: 'travel', keywords: ['mountain', 'nature'] },
157
- { emoji: '\u{1F3D6}', name: 'beach with umbrella', category: 'travel', keywords: ['beach', 'vacation'] },
158
- { emoji: '\u{1F3DD}', name: 'desert island', category: 'travel', keywords: ['island', 'tropical'] },
159
- ],
160
- activities: [
161
- { emoji: '\u26BD', name: 'soccer ball', category: 'activities', keywords: ['soccer', 'football', 'sport'] },
162
- { emoji: '\u{1F3C0}', name: 'basketball', category: 'activities', keywords: ['basketball', 'sport'] },
163
- { emoji: '\u{1F3C8}', name: 'american football', category: 'activities', keywords: ['football', 'sport'] },
164
- { emoji: '\u26BE', name: 'baseball', category: 'activities', keywords: ['baseball', 'sport'] },
165
- { emoji: '\u{1F3BE}', name: 'tennis', category: 'activities', keywords: ['tennis', 'sport'] },
166
- { emoji: '\u{1F3D0}', name: 'volleyball', category: 'activities', keywords: ['volleyball', 'sport'] },
167
- { emoji: '\u{1F3B1}', name: 'pool 8 ball', category: 'activities', keywords: ['pool', 'billiards'] },
168
- { emoji: '\u{1F3D3}', name: 'ping pong', category: 'activities', keywords: ['table tennis', 'sport'] },
169
- { emoji: '\u{1F3C6}', name: 'trophy', category: 'activities', keywords: ['trophy', 'winner', 'award'] },
170
- { emoji: '\u{1F3C5}', name: 'sports medal', category: 'activities', keywords: ['medal', 'winner'] },
171
- { emoji: '\u{1F947}', name: 'first place medal', category: 'activities', keywords: ['gold', 'first', 'winner'] },
172
- { emoji: '\u{1F948}', name: 'second place medal', category: 'activities', keywords: ['silver', 'second'] },
173
- { emoji: '\u{1F949}', name: 'third place medal', category: 'activities', keywords: ['bronze', 'third'] },
174
- { emoji: '\u{1F3AF}', name: 'bullseye', category: 'activities', keywords: ['target', 'dart'] },
175
- { emoji: '\u{1F3AE}', name: 'video game', category: 'activities', keywords: ['game', 'controller'] },
176
- { emoji: '\u{1F3B2}', name: 'game die', category: 'activities', keywords: ['dice', 'game'] },
177
- { emoji: '\u{1F3B5}', name: 'musical note', category: 'activities', keywords: ['music', 'note'] },
178
- { emoji: '\u{1F3B6}', name: 'musical notes', category: 'activities', keywords: ['music', 'notes'] },
179
- { emoji: '\u{1F3A4}', name: 'microphone', category: 'activities', keywords: ['karaoke', 'sing', 'mic'] },
180
- { emoji: '\u{1F3AC}', name: 'clapper board', category: 'activities', keywords: ['movie', 'film'] },
181
- ],
182
- objects: [
183
- { emoji: '\u{1F4F1}', name: 'mobile phone', category: 'objects', keywords: ['phone', 'mobile', 'cell'] },
184
- { emoji: '\u{1F4BB}', name: 'laptop', category: 'objects', keywords: ['laptop', 'computer'] },
185
- { emoji: '\u{1F5A5}', name: 'desktop computer', category: 'objects', keywords: ['computer', 'desktop'] },
186
- { emoji: '\u2328', name: 'keyboard', category: 'objects', keywords: ['keyboard', 'type'] },
187
- { emoji: '\u{1F4F7}', name: 'camera', category: 'objects', keywords: ['camera', 'photo'] },
188
- { emoji: '\u{1F4FA}', name: 'television', category: 'objects', keywords: ['tv', 'television'] },
189
- { emoji: '\u{1F4E7}', name: 'e-mail', category: 'objects', keywords: ['email', 'mail'] },
190
- { emoji: '\u{1F4DD}', name: 'memo', category: 'objects', keywords: ['memo', 'note', 'write'] },
191
- { emoji: '\u{1F4D6}', name: 'open book', category: 'objects', keywords: ['book', 'read'] },
192
- { emoji: '\u{1F4DA}', name: 'books', category: 'objects', keywords: ['books', 'library'] },
193
- { emoji: '\u{1F4C5}', name: 'calendar', category: 'objects', keywords: ['calendar', 'date'] },
194
- { emoji: '\u{1F4CB}', name: 'clipboard', category: 'objects', keywords: ['clipboard', 'paste'] },
195
- { emoji: '\u{1F4CC}', name: 'pushpin', category: 'objects', keywords: ['pin', 'pushpin'] },
196
- { emoji: '\u{1F4CE}', name: 'paperclip', category: 'objects', keywords: ['paperclip', 'attach'] },
197
- { emoji: '\u{1F511}', name: 'key', category: 'objects', keywords: ['key', 'lock'] },
198
- { emoji: '\u{1F512}', name: 'locked', category: 'objects', keywords: ['lock', 'secure'] },
199
- { emoji: '\u{1F513}', name: 'unlocked', category: 'objects', keywords: ['unlock', 'open'] },
200
- { emoji: '\u{1F50D}', name: 'magnifying glass tilted left', category: 'objects', keywords: ['search', 'find'] },
201
- { emoji: '\u{1F4A1}', name: 'light bulb', category: 'objects', keywords: ['idea', 'light'] },
202
- { emoji: '\u{1F527}', name: 'wrench', category: 'objects', keywords: ['tool', 'wrench', 'fix'] },
203
- { emoji: '\u{1F528}', name: 'hammer', category: 'objects', keywords: ['tool', 'hammer', 'build'] },
204
- { emoji: '\u2699', name: 'gear', category: 'objects', keywords: ['settings', 'gear', 'config'] },
205
- ],
206
- symbols: [
207
- { emoji: '\u2764', name: 'red heart', category: 'symbols', keywords: ['heart', 'love'] },
208
- { emoji: '\u{1F9E1}', name: 'orange heart', category: 'symbols', keywords: ['heart', 'orange'] },
209
- { emoji: '\u{1F49B}', name: 'yellow heart', category: 'symbols', keywords: ['heart', 'yellow'] },
210
- { emoji: '\u{1F49A}', name: 'green heart', category: 'symbols', keywords: ['heart', 'green'] },
211
- { emoji: '\u{1F499}', name: 'blue heart', category: 'symbols', keywords: ['heart', 'blue'] },
212
- { emoji: '\u{1F49C}', name: 'purple heart', category: 'symbols', keywords: ['heart', 'purple'] },
213
- { emoji: '\u{1F494}', name: 'broken heart', category: 'symbols', keywords: ['heart', 'broken', 'sad'] },
214
- { emoji: '\u{1F495}', name: 'two hearts', category: 'symbols', keywords: ['heart', 'love'] },
215
- { emoji: '\u{1F4AF}', name: 'hundred points', category: 'symbols', keywords: ['100', 'perfect', 'score'] },
216
- { emoji: '\u{1F4A5}', name: 'collision', category: 'symbols', keywords: ['boom', 'bang', 'explosion'] },
217
- { emoji: '\u{1F4AB}', name: 'dizzy', category: 'symbols', keywords: ['star', 'dizzy'] },
218
- { emoji: '\u2728', name: 'sparkles', category: 'symbols', keywords: ['sparkle', 'shine', 'star'] },
219
- { emoji: '\u{1F525}', name: 'fire', category: 'symbols', keywords: ['fire', 'hot', 'flame'] },
220
- { emoji: '\u2705', name: 'check mark button', category: 'symbols', keywords: ['check', 'done', 'yes'] },
221
- { emoji: '\u274C', name: 'cross mark', category: 'symbols', keywords: ['no', 'wrong', 'x'] },
222
- { emoji: '\u2757', name: 'exclamation mark', category: 'symbols', keywords: ['exclamation', 'warning', 'alert'] },
223
- { emoji: '\u2753', name: 'question mark', category: 'symbols', keywords: ['question', 'what'] },
224
- { emoji: '\u{1F6AB}', name: 'prohibited', category: 'symbols', keywords: ['no', 'forbidden', 'stop'] },
225
- { emoji: '\u267B', name: 'recycling symbol', category: 'symbols', keywords: ['recycle', 'green'] },
226
- { emoji: '\u{1F4AC}', name: 'speech balloon', category: 'symbols', keywords: ['speech', 'chat', 'message'] },
227
- { emoji: '\u{1F4AD}', name: 'thought balloon', category: 'symbols', keywords: ['thought', 'think'] },
228
- { emoji: '\u{1F514}', name: 'bell', category: 'symbols', keywords: ['bell', 'notification', 'alert'] },
229
- ],
230
- flags: [
231
- { emoji: '\u{1F1FA}\u{1F1F8}', name: 'flag: United States', category: 'flags', keywords: ['us', 'usa', 'america'] },
232
- { emoji: '\u{1F1EC}\u{1F1E7}', name: 'flag: United Kingdom', category: 'flags', keywords: ['uk', 'britain', 'england'] },
233
- { emoji: '\u{1F1E8}\u{1F1E6}', name: 'flag: Canada', category: 'flags', keywords: ['canada'] },
234
- { emoji: '\u{1F1E6}\u{1F1FA}', name: 'flag: Australia', category: 'flags', keywords: ['australia'] },
235
- { emoji: '\u{1F1EF}\u{1F1F5}', name: 'flag: Japan', category: 'flags', keywords: ['japan'] },
236
- { emoji: '\u{1F1EB}\u{1F1F7}', name: 'flag: France', category: 'flags', keywords: ['france'] },
237
- { emoji: '\u{1F1E9}\u{1F1EA}', name: 'flag: Germany', category: 'flags', keywords: ['germany'] },
238
- { emoji: '\u{1F1EE}\u{1F1F3}', name: 'flag: India', category: 'flags', keywords: ['india'] },
239
- { emoji: '\u{1F1E7}\u{1F1F7}', name: 'flag: Brazil', category: 'flags', keywords: ['brazil'] },
240
- { emoji: '\u{1F1F0}\u{1F1F7}', name: 'flag: South Korea', category: 'flags', keywords: ['korea', 'south korea'] },
241
- { emoji: '\u{1F1EE}\u{1F1F9}', name: 'flag: Italy', category: 'flags', keywords: ['italy'] },
242
- { emoji: '\u{1F1EA}\u{1F1F8}', name: 'flag: Spain', category: 'flags', keywords: ['spain'] },
243
- { emoji: '\u{1F1F2}\u{1F1FD}', name: 'flag: Mexico', category: 'flags', keywords: ['mexico'] },
244
- { emoji: '\u{1F1F7}\u{1F1FA}', name: 'flag: Russia', category: 'flags', keywords: ['russia'] },
245
- { emoji: '\u{1F1E8}\u{1F1F3}', name: 'flag: China', category: 'flags', keywords: ['china'] },
246
- { emoji: '\u{1F3C1}', name: 'chequered flag', category: 'flags', keywords: ['finish', 'race'] },
247
- { emoji: '\u{1F3F3}', name: 'white flag', category: 'flags', keywords: ['surrender', 'peace'] },
248
- { emoji: '\u{1F3F4}', name: 'black flag', category: 'flags', keywords: ['pirate'] },
249
- { emoji: '\u{1F6A9}', name: 'triangular flag', category: 'flags', keywords: ['flag', 'marker'] },
250
- ],
251
- }
43
+ /** Total number of base emoji shipped. */
44
+ export const EMOJI_COUNT_TOTAL = EMOJI_COUNT
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Decode the generated dataset once.
48
+ // ---------------------------------------------------------------------------
252
49
 
253
- /** All categories in display order */
254
- export const EMOJI_CATEGORIES: EmojiCategory[] = [
255
- 'smileys',
256
- 'people',
257
- 'nature',
258
- 'food',
259
- 'travel',
260
- 'activities',
261
- 'objects',
262
- 'symbols',
263
- 'flags',
264
- ]
50
+ const ALL_EMOJIS: EmojiEntry[] = EMOJI_TUPLES.map((t) => ({
51
+ emoji: t[0],
52
+ name: t[1],
53
+ category: CATEGORY_ORDER[t[2]] as EmojiCategory,
54
+ shortcode: t[3],
55
+ keywords: t[4] ? t[4].split(' ') : [],
56
+ }))
265
57
 
266
- /** Category display labels */
58
+ /** All categories in display order. */
59
+ export const EMOJI_CATEGORIES: EmojiCategory[] = [...CATEGORY_ORDER]
60
+
61
+ /** Category display labels. */
267
62
  export const CATEGORY_LABELS: Record<EmojiCategory, string> = {
268
63
  smileys: 'Smileys & Emotion',
269
64
  people: 'People & Body',
@@ -276,7 +71,206 @@ export const CATEGORY_LABELS: Record<EmojiCategory, string> = {
276
71
  flags: 'Flags',
277
72
  }
278
73
 
279
- /** Get all emojis as a flat array */
74
+ /** Emojis grouped by category (display order preserved within each group). */
75
+ export const EMOJI_DATA: Record<EmojiCategory, EmojiEntry[]> = EMOJI_CATEGORIES.reduce(
76
+ (acc, cat) => {
77
+ acc[cat] = ALL_EMOJIS.filter((e) => e.category === cat)
78
+ return acc
79
+ },
80
+ {} as Record<EmojiCategory, EmojiEntry[]>,
81
+ )
82
+
83
+ /** Get all emojis as a flat array (display order). */
280
84
  export function getAllEmojis(): EmojiEntry[] {
281
- return EMOJI_CATEGORIES.flatMap((cat) => EMOJI_DATA[cat])
85
+ return ALL_EMOJIS
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Shortcodes: derived (from names) + curated popular aliases.
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Curated popular aliases (`:body:` → glyph) that don't match the derived
94
+ * snake_case name — the short forms people actually type (`:joy:`, `:100:`,
95
+ * `:thumbsup:`). These take precedence over derived shortcodes on lookup.
96
+ * This is the ONE place aliases live (rich-editor consumes it, no duplication).
97
+ */
98
+ export const SHORTCODE_ALIASES: Record<string, string> = {
99
+ smile: '\u{1F604}',
100
+ laugh: '\u{1F602}',
101
+ joy: '\u{1F602}',
102
+ grin: '\u{1F601}',
103
+ wink: '\u{1F609}',
104
+ blush: '\u{1F60A}',
105
+ heart_eyes: '\u{1F60D}',
106
+ kissing: '\u{1F617}',
107
+ thinking: '\u{1F914}',
108
+ neutral: '\u{1F610}',
109
+ expressionless: '\u{1F611}',
110
+ unamused: '\u{1F612}',
111
+ sweat: '\u{1F613}',
112
+ pensive: '\u{1F614}',
113
+ confused: '\u{1F615}',
114
+ disappointed: '\u{1F61E}',
115
+ worried: '\u{1F61F}',
116
+ angry: '\u{1F620}',
117
+ rage: '\u{1F621}',
118
+ cry: '\u{1F622}',
119
+ sob: '\u{1F62D}',
120
+ scream: '\u{1F631}',
121
+ fearful: '\u{1F628}',
122
+ cold_sweat: '\u{1F630}',
123
+ relieved: '\u{1F60C}',
124
+ sleepy: '\u{1F62A}',
125
+ sleeping: '\u{1F634}',
126
+ mask: '\u{1F637}',
127
+ sunglasses: '\u{1F60E}',
128
+ nerd: '\u{1F913}',
129
+ heart: '\u{2764}\u{FE0F}',
130
+ broken_heart: '\u{1F494}',
131
+ sparkling_heart: '\u{1F496}',
132
+ two_hearts: '\u{1F495}',
133
+ fire: '\u{1F525}',
134
+ '100': '\u{1F4AF}',
135
+ star: '\u{2B50}',
136
+ star2: '\u{1F31F}',
137
+ sparkles: '\u{2728}',
138
+ thumbsup: '\u{1F44D}',
139
+ thumbsdown: '\u{1F44E}',
140
+ wave: '\u{1F44B}',
141
+ clap: '\u{1F44F}',
142
+ raised_hands: '\u{1F64C}',
143
+ pray: '\u{1F64F}',
144
+ muscle: '\u{1F4AA}',
145
+ point_up: '\u{261D}\u{FE0F}',
146
+ point_down: '\u{1F447}',
147
+ point_left: '\u{1F448}',
148
+ point_right: '\u{1F449}',
149
+ ok_hand: '\u{1F44C}',
150
+ v: '\u{270C}\u{FE0F}',
151
+ eyes: '\u{1F440}',
152
+ tongue: '\u{1F445}',
153
+ lips: '\u{1F444}',
154
+ rocket: '\u{1F680}',
155
+ tada: '\u{1F389}',
156
+ confetti: '\u{1F38A}',
157
+ balloon: '\u{1F388}',
158
+ gift: '\u{1F381}',
159
+ trophy: '\u{1F3C6}',
160
+ medal: '\u{1F3C5}',
161
+ crown: '\u{1F451}',
162
+ gem: '\u{1F48E}',
163
+ bell: '\u{1F514}',
164
+ check: '\u{2705}',
165
+ x: '\u{274C}',
166
+ warning: '\u{26A0}\u{FE0F}',
167
+ no_entry: '\u{26D4}',
168
+ question: '\u{2753}',
169
+ exclamation: '\u{2757}',
170
+ bulb: '\u{1F4A1}',
171
+ book: '\u{1F4D6}',
172
+ memo: '\u{1F4DD}',
173
+ pencil: '\u{270F}\u{FE0F}',
174
+ calendar: '\u{1F4C5}',
175
+ clock: '\u{1F550}',
176
+ hourglass: '\u{231B}',
177
+ lock: '\u{1F512}',
178
+ key: '\u{1F511}',
179
+ hammer: '\u{1F528}',
180
+ wrench: '\u{1F527}',
181
+ link: '\u{1F517}',
182
+ paperclip: '\u{1F4CE}',
183
+ scissors: '\u{2702}\u{FE0F}',
184
+ package: '\u{1F4E6}',
185
+ email: '\u{1F4E7}',
186
+ phone: '\u{1F4DE}',
187
+ computer: '\u{1F4BB}',
188
+ house: '\u{1F3E0}',
189
+ tree: '\u{1F333}',
190
+ sun: '\u{2600}\u{FE0F}',
191
+ moon: '\u{1F319}',
192
+ cloud: '\u{2601}\u{FE0F}',
193
+ rain: '\u{1F327}\u{FE0F}',
194
+ snow: '\u{2744}\u{FE0F}',
195
+ zap: '\u{26A1}',
196
+ rainbow: '\u{1F308}',
197
+ pizza: '\u{1F355}',
198
+ hamburger: '\u{1F354}',
199
+ coffee: '\u{2615}',
200
+ beer: '\u{1F37A}',
201
+ wine: '\u{1F377}',
202
+ cake: '\u{1F370}',
203
+ dog: '\u{1F436}',
204
+ cat: '\u{1F431}',
205
+ poop: '\u{1F4A9}',
206
+ ghost: '\u{1F47B}',
207
+ skull: '\u{1F480}',
208
+ alien: '\u{1F47D}',
209
+ robot: '\u{1F916}',
210
+ handshake: '\u{1F91D}',
211
+ salute: '\u{1FAE1}',
212
+ }
213
+
214
+ /** shortcode body → EmojiEntry, for the derived (name-based) shortcodes. */
215
+ const BY_SHORTCODE = new Map<string, EmojiEntry>()
216
+ for (const e of ALL_EMOJIS) BY_SHORTCODE.set(e.shortcode, e)
217
+ /** glyph → EmojiEntry (for alias resolution back to a full entry). */
218
+ const BY_GLYPH = new Map<string, EmojiEntry>()
219
+ for (const e of ALL_EMOJIS) if (!BY_GLYPH.has(e.emoji)) BY_GLYPH.set(e.emoji, e)
220
+
221
+ /**
222
+ * Resolve a `:shortcode:` (with or without surrounding colons) to its glyph.
223
+ * Aliases win over derived shortcodes. Returns `null` when unknown.
224
+ */
225
+ export function resolveShortcode(code: string): string | null {
226
+ const body = code.replace(/^:/, '').replace(/:$/, '').toLowerCase()
227
+ if (body in SHORTCODE_ALIASES) return SHORTCODE_ALIASES[body]
228
+ const entry = BY_SHORTCODE.get(body)
229
+ return entry ? entry.emoji : null
230
+ }
231
+
232
+ /**
233
+ * `:body:` → glyph map covering the full derived set plus curated aliases.
234
+ * This is the shared source rich-editor's `EMOJI_MAP` is built from.
235
+ */
236
+ export function buildShortcodeMap(): Record<string, string> {
237
+ const map: Record<string, string> = {}
238
+ for (const e of ALL_EMOJIS) map[`:${e.shortcode}:`] = e.emoji
239
+ for (const [body, glyph] of Object.entries(SHORTCODE_ALIASES)) map[`:${body}:`] = glyph
240
+ return map
241
+ }
242
+
243
+ /**
244
+ * Fuzzy-search emoji by name, keyword, or shortcode. Prefix matches on the
245
+ * shortcode/name rank first. Used by the picker search and the `:` trigger.
246
+ */
247
+ export function searchEmojis(query: string, limit = 50): EmojiEntry[] {
248
+ const q = query.toLowerCase().replace(/^:/, '').replace(/:$/, '').trim()
249
+ if (q === '') return ALL_EMOJIS.slice(0, limit)
250
+
251
+ const scored: { entry: EmojiEntry; score: number }[] = []
252
+ for (const entry of ALL_EMOJIS) {
253
+ const sc = entry.shortcode
254
+ const name = entry.name.toLowerCase()
255
+ let score = -1
256
+ if (sc === q) score = 100
257
+ else if (sc.startsWith(q)) score = 80
258
+ else if (name.startsWith(q)) score = 70
259
+ else if (sc.includes(q)) score = 50
260
+ else if (name.includes(q)) score = 40
261
+ else if (entry.keywords.some((k) => k.startsWith(q))) score = 30
262
+ else if (entry.keywords.some((k) => k.includes(q))) score = 15
263
+ if (score >= 0) scored.push({ entry, score })
264
+ }
265
+ // Include alias-only matches (e.g. `joy`) that point at a known glyph.
266
+ for (const [body, glyph] of Object.entries(SHORTCODE_ALIASES)) {
267
+ if (!body.includes(q)) continue
268
+ const entry = BY_GLYPH.get(glyph)
269
+ if (!entry) continue
270
+ if (scored.some((s) => s.entry === entry)) continue
271
+ scored.push({ entry, score: body === q ? 95 : body.startsWith(q) ? 60 : 20 })
272
+ }
273
+
274
+ scored.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name))
275
+ return scored.slice(0, limit).map((s) => s.entry)
282
276
  }