goemoji 0.3.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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/README.ru.md +258 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/data/en.json +1 -0
- package/data/ru.json +1 -0
- package/dist/discord.d.ts +15 -0
- package/dist/discord.js +2 -0
- package/dist/discord.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +90 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Формат: [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
|
4
|
+
версии — [Semantic Versioning](https://semver.org/lang/ru/).
|
|
5
|
+
|
|
6
|
+
## [0.3.0] — 2026-09-25
|
|
7
|
+
|
|
8
|
+
### Добавлено
|
|
9
|
+
|
|
10
|
+
- `useEmojiData` возвращает `loading` и сбрасывает данные при смене `deps`.
|
|
11
|
+
- Меню тонов кожи управляется с клавиатуры (стрелки, Home/End, Escape).
|
|
12
|
+
|
|
13
|
+
### Исправлено
|
|
14
|
+
|
|
15
|
+
- `columns <= 0` больше не зацикливает раскладку сетки; некорректные
|
|
16
|
+
`columns` и `cellSize` приводятся к безопасным значениям.
|
|
17
|
+
- `sameEmojiValue` больше не путает id серверного эмодзи с его суффиксом.
|
|
18
|
+
- Смена `recentKey` перечитывает «Недавние» из localStorage.
|
|
19
|
+
- Активная ячейка сбрасывается при смене данных и раскладки.
|
|
20
|
+
- Escape закрывает открытое меню тонов, не закрывая весь пикер.
|
|
21
|
+
- Анимация скролла учитывает `prefers-reduced-motion`.
|
|
22
|
+
- `customEmojiUrl` клампит размер картинки к диапазону Discord.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] — 2026-09-22
|
|
25
|
+
|
|
26
|
+
### Изменено
|
|
27
|
+
|
|
28
|
+
- Секция и вкладка серверных эмодзи переехали в начало списка.
|
|
29
|
+
- Вкладки категорий помещаются в одну строку.
|
|
30
|
+
- Фон поповера тонов кожи сделан непрозрачным.
|
|
31
|
+
|
|
32
|
+
### Производительность
|
|
33
|
+
|
|
34
|
+
- Плавный доезд до секции вместо нативного `scroll-behavior: smooth`.
|
|
35
|
+
- Окно виртуализации пересчитывается в rAF, перерисовка — только при сдвиге.
|
|
36
|
+
|
|
37
|
+
## [0.1.0] — 2026-09-22
|
|
38
|
+
|
|
39
|
+
Первый выпуск: виртуализированный эмодзи-пикер на React с вкладками категорий,
|
|
40
|
+
поиском по названиям и тегам, тонами кожи, «Недавними» и серверными эмодзи Discord.
|
|
41
|
+
Словари ru/en собраны из emojibase-data.
|
|
42
|
+
|
|
43
|
+
[0.3.0]: https://github.com/VolRencs/goemoji/compare/v0.2.0...v0.3.0
|
|
44
|
+
[0.2.0]: https://github.com/VolRencs/goemoji/compare/v0.1.0...v0.2.0
|
|
45
|
+
[0.1.0]: https://github.com/VolRencs/goemoji/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VolRencs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# goemoji
|
|
2
|
+
|
|
3
|
+
An emoji picker for React 19 with category tabs, search by name and tags, skin
|
|
4
|
+
tones, recents, and Discord server emoji. The only dependency is `react` (a peer
|
|
5
|
+
dependency). The `ru`/`en` dictionaries ship inside the package, so nothing is
|
|
6
|
+
fetched from a third-party CDN at runtime.
|
|
7
|
+
|
|
8
|
+
The build is about 6.2 KB gzip of JavaScript and 1.2 KB gzip of CSS. The
|
|
9
|
+
dictionaries are 50.2 KB gzip (`ru`) and 38.1 KB gzip (`en`) and are loaded
|
|
10
|
+
lazily, so they never end up in the initial bundle.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- 1914 emoji per locale across 9 categories, with tabs and sticky section headers.
|
|
15
|
+
- Search over localized names and tags, with exact matches ranked first. Server
|
|
16
|
+
emoji are searchable alongside Unicode and get their own section.
|
|
17
|
+
- Skin tones: one base glyph plus five modifiers. Variants are generated at
|
|
18
|
+
runtime instead of being stored, and are checked against emojibase in the test
|
|
19
|
+
suite (1650+ comparisons, no mismatches).
|
|
20
|
+
- Recents: up to 24 emoji in `localStorage`, with a configurable key (for
|
|
21
|
+
example, one per Discord guild).
|
|
22
|
+
- Server emoji rendered as images, searchable, with an optional guild icon on
|
|
23
|
+
the tab.
|
|
24
|
+
- A row-virtualized list: while scrolling, React re-renders only the changed
|
|
25
|
+
window, and tab switches are instant.
|
|
26
|
+
- Keyboard navigation and ARIA markup for `combobox`, `listbox`, `tablist`, and
|
|
27
|
+
`menu`.
|
|
28
|
+
- SSR-friendly: components carry `"use client"`, and data is loaded on the
|
|
29
|
+
client through `useEmojiData`.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install goemoji
|
|
35
|
+
pnpm add goemoji
|
|
36
|
+
yarn add goemoji
|
|
37
|
+
bun add goemoji
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires React 19 or newer and a bundler that understands `exports` (Vite,
|
|
41
|
+
Next.js, webpack 5+, Rollup). The package is ESM-only and ships its own type
|
|
42
|
+
declarations.
|
|
43
|
+
|
|
44
|
+
Install straight from the repository by tag:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add github:VolRencs/goemoji#v0.3.0
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { useState } from "react";
|
|
54
|
+
import { EmojiPicker, useEmojiData, type Emoji, type ServerEmoji } from "goemoji";
|
|
55
|
+
import "goemoji/styles.css";
|
|
56
|
+
|
|
57
|
+
const serverEmojis: ServerEmoji[] = [
|
|
58
|
+
{ id: "100000000000000001", name: "party_parrot", animated: false },
|
|
59
|
+
{ id: "100000000000000002", name: "cat_jam", animated: true },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export function ReactionPicker() {
|
|
63
|
+
const [picked, setPicked] = useState("");
|
|
64
|
+
const { data, error, loading } = useEmojiData(() => import("goemoji/data/ru.json"));
|
|
65
|
+
|
|
66
|
+
if (error) return <p>Could not load emoji: {error.message}</p>;
|
|
67
|
+
if (loading || !data) return <p>Loading emoji…</p>;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<EmojiPicker
|
|
71
|
+
data={data}
|
|
72
|
+
serverEmojis={serverEmojis}
|
|
73
|
+
recentKey="guild:123"
|
|
74
|
+
onSelect={(emoji: Emoji) => setPicked(emoji.value)}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`onSelect` receives an `Emoji`. For Unicode, `value` is the glyph itself
|
|
81
|
+
(`"😀"`); for a server emoji it is a ready-to-store Discord value
|
|
82
|
+
(`"<:name:id>"`, or `"<a:name:id>"` when animated).
|
|
83
|
+
|
|
84
|
+
If the dictionary is known ahead of time, import it statically:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import ru from "goemoji/data/ru.json";
|
|
88
|
+
import { parseEmojiData } from "goemoji";
|
|
89
|
+
|
|
90
|
+
const data = parseEmojiData(ru);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`parseEmojiData` accepts both the JSON object itself and the namespace object
|
|
94
|
+
(`{ default: … }`) returned by `import()`.
|
|
95
|
+
|
|
96
|
+
## Data
|
|
97
|
+
|
|
98
|
+
`data/ru.json` and `data/en.json` are generated from
|
|
99
|
+
[`emojibase-data`](https://github.com/milesj/emojibase) by
|
|
100
|
+
`scripts/build-data.ts` and shipped with the package. The format (v1) is:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"v": 1,
|
|
105
|
+
"locale": "ru",
|
|
106
|
+
"categories": [{ "key": "smileys-emotion", "label": "Смайлики и люди" }],
|
|
107
|
+
"emojis": [["😀", "улыбающееся лицо", "улыбка смех", 0]]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Each emoji row is `[emoji, label, space-separated tags, category index]`.
|
|
112
|
+
Skin-tone components (the `component` group) and standalone regional indicators
|
|
113
|
+
are excluded; full flags remain. Skin-tone variants are produced at runtime by
|
|
114
|
+
`skinToneVariation`.
|
|
115
|
+
|
|
116
|
+
You can build a custom dictionary in the same format and pass it to
|
|
117
|
+
`parseEmojiData`; the component does not require data to come from this package.
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### `EmojiPicker`
|
|
122
|
+
|
|
123
|
+
| Prop | Type | Default | Description |
|
|
124
|
+
| --- | --- | --- | --- |
|
|
125
|
+
| `data` | `EmojiData` | — | Parsed dictionary: `parseEmojiData(...)` or the result of `useEmojiData` |
|
|
126
|
+
| `serverEmojis` | `readonly ServerEmoji[]` | — | Server emoji: their own tab and search entries |
|
|
127
|
+
| `columns` | `number` | `8` | Grid columns; minimum 1, `NaN` falls back to the default |
|
|
128
|
+
| `cellSize` | `number` | `34` | Cell size in pixels; minimum 1, `NaN` falls back to the default |
|
|
129
|
+
| `locale` | `string` | `"ru"` | Built-in label locale: `ru` or `en` |
|
|
130
|
+
| `labels` | `Partial<Labels>` | — | Overrides for individual UI strings |
|
|
131
|
+
| `skinTone` | `SkinTone` | — | Controlled tone; when omitted, the tone is kept in `localStorage` |
|
|
132
|
+
| `onSkinToneChange` | `(tone: SkinTone) => void` | — | Called when the tone changes |
|
|
133
|
+
| `recentKey` | `string \| false` | `"goemoji:recent"` | `localStorage` key for recents; `false` disables them |
|
|
134
|
+
| `onSelect` | `(emoji: Emoji) => void` | — | Required selection handler |
|
|
135
|
+
| `onEscape` | `() => void` | — | Called on Escape once the tone menu is closed |
|
|
136
|
+
| `serverIconUrl` | `string` | — | Image used on the server tab instead of the default icon |
|
|
137
|
+
| `className` | `string` | — | Extra class on the root `.ge-root` element |
|
|
138
|
+
|
|
139
|
+
The selected tone is stored under the `goemoji:tone` `localStorage` key unless
|
|
140
|
+
`skinTone` is passed. `SkinTone` is one of `"none"`, `"light"`,
|
|
141
|
+
`"medium-light"`, `"medium"`, `"medium-dark"`, `"dark"`.
|
|
142
|
+
|
|
143
|
+
### `useEmojiData`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const { data, error, loading } = useEmojiData(load, deps?);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Loads a dictionary and parses it with `parseEmojiData`. The loader runs on mount
|
|
150
|
+
and again whenever `deps` change; an in-flight request is ignored, and data is
|
|
151
|
+
reset to `null` until the new one resolves. Effects do not run during SSR, so
|
|
152
|
+
`loading` stays `true` there.
|
|
153
|
+
|
|
154
|
+
### Types
|
|
155
|
+
|
|
156
|
+
| Type | Fields |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `Emoji` | `value: string`, `label: string`, `tags: string`, `category: number`, `server?: { id: string; animated: boolean }` |
|
|
159
|
+
| `ServerEmoji` | `id: string`, `name: string`, `animated: boolean` |
|
|
160
|
+
| `EmojiData` | `locale: string`, `categories: Category[]`, `emojis: Emoji[]` |
|
|
161
|
+
| `Category` | `key: string`, `label: string` |
|
|
162
|
+
| `SlimEmoji` | `[emoji, label, tags, category]` — a raw dictionary row |
|
|
163
|
+
| `Labels` | `search`, `empty`, `recent`, `server`, `skinTone`, `skinTones` |
|
|
164
|
+
|
|
165
|
+
All types, including `EmojiPickerProps`, are exported from the package root.
|
|
166
|
+
|
|
167
|
+
### `goemoji/discord`
|
|
168
|
+
|
|
169
|
+
A React-free entry point for bots, API routes, and reaction handling.
|
|
170
|
+
|
|
171
|
+
| Function | Description |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `parseCustomEmoji(value)` | Parses `<:name:id>` / `<a:name:id>`; returns `null` for Unicode |
|
|
174
|
+
| `customEmojiToString({ name, id, animated? })` | Builds `<a:name:id>` / `<:name:id>` |
|
|
175
|
+
| `customEmojiUrl(id, animated?, size?)` | Discord CDN URL; `size` is clamped to 16–4096 and defaults to 48 |
|
|
176
|
+
| `normalizeEmojiText(value)` | Strips U+FE0E/U+FE0F variation selectors and ZWJ |
|
|
177
|
+
| `sameEmojiValue(saved, name, identifier)` | Matches a stored value against a reaction: custom emoji by id, Unicode by normalized text |
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { sameEmojiValue } from "goemoji/discord";
|
|
181
|
+
|
|
182
|
+
sameEmojiValue("<a:blob:777>", "blob", "blob:777"); // true
|
|
183
|
+
sameEmojiValue("❤️", "❤", "❤"); // true
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Keyboard
|
|
187
|
+
|
|
188
|
+
| Key | Action |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `←` / `→` | Previous or next emoji; wraps to the adjacent row at the edge |
|
|
191
|
+
| `↑` / `↓` | Row above or below, keeping the column where possible |
|
|
192
|
+
| `PageUp` / `PageDown` | Five rows up or down |
|
|
193
|
+
| `Home` / `End` | First or last emoji in the list |
|
|
194
|
+
| `Enter` | Select the active emoji |
|
|
195
|
+
| `Esc` | Close the tone menu, then call `onEscape` |
|
|
196
|
+
|
|
197
|
+
Focus stays in the search field; the list is linked to it through
|
|
198
|
+
`aria-activedescendant`, following the combobox pattern. While the tone menu is
|
|
199
|
+
open, arrows, `Home`, `End`, and `Esc` control the menu rather than the list.
|
|
200
|
+
|
|
201
|
+
## Styling
|
|
202
|
+
|
|
203
|
+
The theme is driven by CSS variables on `.ge-root` (or any ancestor). No
|
|
204
|
+
`!important` is used:
|
|
205
|
+
|
|
206
|
+
| Variable | Default | Purpose |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| `--ge-bg` | `#1e1f22` | Picker background |
|
|
209
|
+
| `--ge-panel` | `#2b2d31` | Search field and buttons |
|
|
210
|
+
| `--ge-surface` | `#232428` | Tone popover |
|
|
211
|
+
| `--ge-border` | `#1a1b1e` | Borders |
|
|
212
|
+
| `--ge-text` | `#dbdee1` | Primary text |
|
|
213
|
+
| `--ge-muted` | `#949ba4` | Captions and section headers |
|
|
214
|
+
| `--ge-hover` | `rgba(255, 255, 255, 0.06)` | Hover state |
|
|
215
|
+
| `--ge-active` | `rgba(88, 101, 242, 0.4)` | Active cell |
|
|
216
|
+
| `--ge-accent` | `#5865f2` | Accent, focus ring, active tab |
|
|
217
|
+
| `--ge-radius` | `6px` | Corner radius |
|
|
218
|
+
| `--ge-list-height` | `320px` | List height |
|
|
219
|
+
| `--ge-font` | system stack | UI font |
|
|
220
|
+
| `--ge-font-emoji` | system emoji stack | Unicode emoji font |
|
|
221
|
+
|
|
222
|
+
```css
|
|
223
|
+
.my-picker {
|
|
224
|
+
--ge-accent: #f2a65a;
|
|
225
|
+
--ge-list-height: 420px;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Every element uses a `ge-` prefixed class (`ge-root`, `ge-search`, `ge-list`,
|
|
230
|
+
`ge-cell`, `ge-tabs`, and so on), so targeted overrides are possible too, but
|
|
231
|
+
the variables cover most cases.
|
|
232
|
+
|
|
233
|
+
## Accessibility
|
|
234
|
+
|
|
235
|
+
- The search field is a `combobox` with `aria-controls` and
|
|
236
|
+
`aria-activedescendant`; the list is a `listbox`, and cells are `option`
|
|
237
|
+
elements with `aria-selected`.
|
|
238
|
+
- Category tabs use `tablist`/`tab` with `aria-selected`.
|
|
239
|
+
- The tone menu uses `menu`/`menuitemradio` with `aria-checked`; focus returns
|
|
240
|
+
to the button on selection or close. Navigation supports arrows, `Home`,
|
|
241
|
+
`End`, and `Esc`.
|
|
242
|
+
- Clicking outside the popover closes it without stealing focus.
|
|
243
|
+
- The section-scroll animation is disabled under
|
|
244
|
+
`prefers-reduced-motion: reduce`.
|
|
245
|
+
- Built-in labels are provided for `ru` and `en`; pass `labels` for anything
|
|
246
|
+
else.
|
|
247
|
+
|
|
248
|
+
## Performance
|
|
249
|
+
|
|
250
|
+
The list renders row by row: only the rows inside the virtualization window,
|
|
251
|
+
plus a small overscan, exist in the DOM. On scroll the range is recomputed in a
|
|
252
|
+
`requestAnimationFrame` callback, and React receives new state only when the
|
|
253
|
+
window actually moves. Section headers stick to the top edge, and clicking a tab
|
|
254
|
+
highlights it immediately and glides to the section with a short animation.
|
|
255
|
+
|
|
256
|
+
Search runs over the in-memory emoji array (1914 entries per locale), so results
|
|
257
|
+
update on every keystroke without debouncing.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
Code is MIT. Emoji names and tags come from
|
|
262
|
+
[emojibase](https://github.com/milesj/emojibase) (MIT) and the Unicode CLDR
|
|
263
|
+
annotations (Unicode License v3); see `THIRD_PARTY_NOTICES.md` for details.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# goemoji
|
|
2
|
+
|
|
3
|
+
Пикер эмодзи для React 19: категории с вкладками, поиск по названию и тегам,
|
|
4
|
+
тона кожи, «Недавние» и серверные эмодзи Discord. Единственная зависимость —
|
|
5
|
+
`react` (peer), словари `ru`/`en` лежат внутри пакета, внешние CDN в рантайме не
|
|
6
|
+
нужны.
|
|
7
|
+
|
|
8
|
+
Размер сборки: около 6.2 КБ gzip JS и 1.2 КБ gzip CSS. Словари занимают
|
|
9
|
+
50.2 КБ gzip (`ru`) и 38.1 КБ gzip (`en`), но подключаются лениво, поэтому в
|
|
10
|
+
первый бандл не попадают.
|
|
11
|
+
|
|
12
|
+
## Возможности
|
|
13
|
+
|
|
14
|
+
- 1914 эмодзи в каждой локали, 9 категорий с вкладками и липкими заголовками.
|
|
15
|
+
- Поиск по локализованному названию и тегам с приоритетом точных совпадений;
|
|
16
|
+
серверные эмодзи ищутся вместе с юникодом и получают отдельную секцию.
|
|
17
|
+
- Тона кожи: базовый глиф и пять модификаторов. Варианты не хранятся в данных,
|
|
18
|
+
а собираются на лету и сверены с emojibase (1650+ сравнений, расхождений нет).
|
|
19
|
+
- «Недавние» — до 24 эмодзи в localStorage; ключ можно задать свой, например
|
|
20
|
+
для каждого сервера Discord.
|
|
21
|
+
- Серверные эмодзи рендерятся картинкой, участвуют в поиске и могут иметь
|
|
22
|
+
собственную иконку на вкладке.
|
|
23
|
+
- Список виртуализирован по строкам: при скролле React перерисовывает только
|
|
24
|
+
изменившееся окно, вкладки переключаются мгновенно.
|
|
25
|
+
- Управление с клавиатуры и ARIA-разметка `combobox`/`listbox`/`tablist`/`menu`.
|
|
26
|
+
- SSR-совместимость: компоненты помечены `"use client"`, данные грузятся на
|
|
27
|
+
клиенте через `useEmojiData`.
|
|
28
|
+
|
|
29
|
+
## Установка
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install goemoji
|
|
33
|
+
pnpm add goemoji
|
|
34
|
+
yarn add goemoji
|
|
35
|
+
bun add goemoji
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Нужен React 19 или новее и сборщик, понимающий `exports` (Vite, Next.js,
|
|
39
|
+
webpack 5+, Rollup). Пакет поставляется как ESM вместе с объявлениями типов.
|
|
40
|
+
|
|
41
|
+
Установка напрямую из репозитория по тегу:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm add github:VolRencs/goemoji#v0.3.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Быстрый старт
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useState } from "react";
|
|
51
|
+
import { EmojiPicker, useEmojiData, type Emoji, type ServerEmoji } from "goemoji";
|
|
52
|
+
import "goemoji/styles.css";
|
|
53
|
+
|
|
54
|
+
const serverEmojis: ServerEmoji[] = [
|
|
55
|
+
{ id: "100000000000000001", name: "party_parrot", animated: false },
|
|
56
|
+
{ id: "100000000000000002", name: "cat_jam", animated: true },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function ReactionPicker() {
|
|
60
|
+
const [picked, setPicked] = useState("");
|
|
61
|
+
const { data, error, loading } = useEmojiData(() => import("goemoji/data/ru.json"));
|
|
62
|
+
|
|
63
|
+
if (error) return <p>Не удалось загрузить эмодзи: {error.message}</p>;
|
|
64
|
+
if (loading || !data) return <p>Загружаем эмодзи…</p>;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<EmojiPicker
|
|
68
|
+
data={data}
|
|
69
|
+
serverEmojis={serverEmojis}
|
|
70
|
+
recentKey="guild:123"
|
|
71
|
+
onSelect={(emoji: Emoji) => setPicked(emoji.value)}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`onSelect` получает объект `Emoji`. У юникода поле `value` содержит глиф
|
|
78
|
+
(`"😀"`), у серверного эмодзи — готовую строку для Discord API и БД
|
|
79
|
+
(`"<:name:id>"` или `"<a:name:id>"` для анимированного).
|
|
80
|
+
|
|
81
|
+
Если словарь известен заранее, его можно импортировать статически:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import ru from "goemoji/data/ru.json";
|
|
85
|
+
import { parseEmojiData } from "goemoji";
|
|
86
|
+
|
|
87
|
+
const data = parseEmojiData(ru);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`parseEmojiData` принимает как сам JSON-объект, так и namespace ES-модуля вида
|
|
91
|
+
`{ default: … }`, который возвращает `import()`.
|
|
92
|
+
|
|
93
|
+
## Данные
|
|
94
|
+
|
|
95
|
+
Файлы `data/ru.json` и `data/en.json` собираются из
|
|
96
|
+
[`emojibase-data`](https://github.com/milesj/emojibase) скриптом
|
|
97
|
+
`scripts/build-data.ts` и лежат в пакете. Формат (v1):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"v": 1,
|
|
102
|
+
"locale": "ru",
|
|
103
|
+
"categories": [{ "key": "smileys-emotion", "label": "Смайлики и люди" }],
|
|
104
|
+
"emojis": [["😀", "улыбающееся лицо", "улыбка смех", 0]]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Строка эмодзи — это `[эмодзи, название, теги через пробел, индекс категории]`.
|
|
109
|
+
Из данных исключены компоненты тонов кожи (группа `component`) и региональные
|
|
110
|
+
индикаторы без группы: самостоятельных флагов в списке нет, а тона собираются
|
|
111
|
+
функцией `skinToneVariation` в рантайме.
|
|
112
|
+
|
|
113
|
+
Свой словарь можно собрать в том же формате и передать в `parseEmojiData` —
|
|
114
|
+
компонент не требует, чтобы данные пришли именно из этого пакета.
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### `EmojiPicker`
|
|
119
|
+
|
|
120
|
+
| Проп | Тип | По умолчанию | Описание |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `data` | `EmojiData` | — | Разобранный словарь: `parseEmojiData(...)` или результат `useEmojiData` |
|
|
123
|
+
| `serverEmojis` | `readonly ServerEmoji[]` | — | Серверные эмодзи: отдельная вкладка и участие в поиске |
|
|
124
|
+
| `columns` | `number` | `8` | Колонок в сетке; минимум 1, `NaN` заменяется на значение по умолчанию |
|
|
125
|
+
| `cellSize` | `number` | `34` | Сторона ячейки в пикселях; минимум 1, `NaN` заменяется на значение по умолчанию |
|
|
126
|
+
| `locale` | `string` | `"ru"` | Локаль встроенных подписей: `ru` или `en` |
|
|
127
|
+
| `labels` | `Partial<Labels>` | — | Переопределение отдельных строк интерфейса |
|
|
128
|
+
| `skinTone` | `SkinTone` | — | Контролируемый тон кожи; без пропа хранится в localStorage |
|
|
129
|
+
| `onSkinToneChange` | `(tone: SkinTone) => void` | — | Вызывается при смене тона |
|
|
130
|
+
| `recentKey` | `string \| false` | `"goemoji:recent"` | Ключ localStorage для «Недавних»; `false` отключает их |
|
|
131
|
+
| `onSelect` | `(emoji: Emoji) => void` | — | Обязательный обработчик выбора |
|
|
132
|
+
| `onEscape` | `() => void` | — | Вызывается после Escape, если меню тонов закрыто |
|
|
133
|
+
| `serverIconUrl` | `string` | — | Картинка для вкладки «Сервер» вместо стандартной иконки |
|
|
134
|
+
| `className` | `string` | — | Дополнительный класс на корневой элемент `.ge-root` |
|
|
135
|
+
|
|
136
|
+
Тон кожи хранится в localStorage под ключом `goemoji:tone`, пока не передан
|
|
137
|
+
проп `skinTone`. Значения `SkinTone`: `"none"`, `"light"`, `"medium-light"`,
|
|
138
|
+
`"medium"`, `"medium-dark"`, `"dark"`.
|
|
139
|
+
|
|
140
|
+
### `useEmojiData`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const { data, error, loading } = useEmojiData(load, deps?);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Загружает словарь и разбирает его через `parseEmojiData`. Загрузчик вызывается
|
|
147
|
+
при монтировании и заново при изменении `deps`; незавершённый запрос
|
|
148
|
+
игнорируется, данные сбрасываются в `null` до прихода новых. В SSR эффект не
|
|
149
|
+
выполняется, поэтому `loading` остаётся `true`.
|
|
150
|
+
|
|
151
|
+
### Типы
|
|
152
|
+
|
|
153
|
+
| Тип | Поля |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `Emoji` | `value: string`, `label: string`, `tags: string`, `category: number`, `server?: { id: string; animated: boolean }` |
|
|
156
|
+
| `ServerEmoji` | `id: string`, `name: string`, `animated: boolean` |
|
|
157
|
+
| `EmojiData` | `locale: string`, `categories: Category[]`, `emojis: Emoji[]` |
|
|
158
|
+
| `Category` | `key: string`, `label: string` |
|
|
159
|
+
| `SlimEmoji` | `[emoji, label, tags, category]` — строка сырого словаря |
|
|
160
|
+
| `Labels` | `search`, `empty`, `recent`, `server`, `skinTone`, `skinTones` |
|
|
161
|
+
|
|
162
|
+
Все типы экспортируются из корня пакета вместе с `EmojiPickerProps`.
|
|
163
|
+
|
|
164
|
+
### `goemoji/discord`
|
|
165
|
+
|
|
166
|
+
Модуль без React: подходит для ботов, API-роутов и обработки реакций.
|
|
167
|
+
|
|
168
|
+
| Функция | Описание |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `parseCustomEmoji(value)` | Разбирает `<:name:id>` / `<a:name:id>`, для юникода вернёт `null` |
|
|
171
|
+
| `customEmojiToString({ name, id, animated? })` | Собирает строку `<a:name:id>` / `<:name:id>` |
|
|
172
|
+
| `customEmojiUrl(id, animated?, size?)` | Ссылка на CDN Discord; размер клампится к 16…4096, по умолчанию 48 |
|
|
173
|
+
| `normalizeEmojiText(value)` | Убирает вариационные селекторы U+FE0E/U+FE0F и ZWJ |
|
|
174
|
+
| `sameEmojiValue(saved, name, identifier)` | Сравнивает сохранённое значение с реакцией: серверные — по id, юникод — по нормализованному виду |
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { sameEmojiValue } from "goemoji/discord";
|
|
178
|
+
|
|
179
|
+
sameEmojiValue("<a:blob:777>", "blob", "blob:777"); // true
|
|
180
|
+
sameEmojiValue("❤️", "❤", "❤"); // true
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Клавиатура
|
|
184
|
+
|
|
185
|
+
| Клавиша | Действие |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `←` / `→` | Предыдущий или следующий эмодзи; на краю строки — переход на соседнюю |
|
|
188
|
+
| `↑` / `↓` | Строка выше или ниже с выравниванием по колонке |
|
|
189
|
+
| `PageUp` / `PageDown` | На пять строк вверх или вниз |
|
|
190
|
+
| `Home` / `End` | Первый или последний эмодзи списка |
|
|
191
|
+
| `Enter` | Выбрать активный эмодзи |
|
|
192
|
+
| `Esc` | Закрыть меню тонов, затем вызвать `onEscape` |
|
|
193
|
+
|
|
194
|
+
Фокус остаётся в поле поиска: список связан с ним через
|
|
195
|
+
`aria-activedescendant`, как и положено combobox. При открытом меню тонов
|
|
196
|
+
стрелки, `Home`, `End` и `Esc` управляют меню, а не списком.
|
|
197
|
+
|
|
198
|
+
## Стилизация
|
|
199
|
+
|
|
200
|
+
Тема задаётся CSS-переменными на `.ge-root` (или на любом родителе) и не
|
|
201
|
+
использует `!important`:
|
|
202
|
+
|
|
203
|
+
| Переменная | По умолчанию | Назначение |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `--ge-bg` | `#1e1f22` | Фон пикера |
|
|
206
|
+
| `--ge-panel` | `#2b2d31` | Поле поиска, кнопки |
|
|
207
|
+
| `--ge-surface` | `#232428` | Поповер тонов |
|
|
208
|
+
| `--ge-border` | `#1a1b1e` | Рамки |
|
|
209
|
+
| `--ge-text` | `#dbdee1` | Основной текст |
|
|
210
|
+
| `--ge-muted` | `#949ba4` | Подписи и заголовки секций |
|
|
211
|
+
| `--ge-hover` | `rgba(255, 255, 255, 0.06)` | Наведение |
|
|
212
|
+
| `--ge-active` | `rgba(88, 101, 242, 0.4)` | Активная ячейка |
|
|
213
|
+
| `--ge-accent` | `#5865f2` | Акцент, фокус, активная вкладка |
|
|
214
|
+
| `--ge-radius` | `6px` | Скругление углов |
|
|
215
|
+
| `--ge-list-height` | `320px` | Высота списка |
|
|
216
|
+
| `--ge-font` | системный стек | Шрифт интерфейса |
|
|
217
|
+
| `--ge-font-emoji` | системный эмодзи-стек | Шрифт юникод-эмодзи |
|
|
218
|
+
|
|
219
|
+
```css
|
|
220
|
+
.my-picker {
|
|
221
|
+
--ge-accent: #f2a65a;
|
|
222
|
+
--ge-list-height: 420px;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Все элементы используют классы с префиксом `ge-` (`ge-root`, `ge-search`,
|
|
227
|
+
`ge-list`, `ge-cell`, `ge-tabs` и т. д.), поэтому точечные правки тоже
|
|
228
|
+
возможны, но обычно достаточно переменных.
|
|
229
|
+
|
|
230
|
+
## Доступность
|
|
231
|
+
|
|
232
|
+
- Поле поиска — `role="combobox"` с `aria-controls` и `aria-activedescendant`,
|
|
233
|
+
список — `role="listbox"`, ячейки — `role="option"` с `aria-selected`.
|
|
234
|
+
- Вкладки категорий — `tablist`/`tab` с `aria-selected`.
|
|
235
|
+
- Меню тонов — `menu`/`menuitemradio` с `aria-checked`; фокус возвращается на
|
|
236
|
+
кнопку при выборе и закрытии. Навигация: стрелки, `Home`, `End`, `Esc`.
|
|
237
|
+
- Клик вне поповера закрывает его, не перехватывая фокус.
|
|
238
|
+
- Анимация перехода к секции отключается при `prefers-reduced-motion: reduce`.
|
|
239
|
+
- Подписи по умолчанию есть для `ru` и `en`, остальные можно передать через
|
|
240
|
+
`labels`.
|
|
241
|
+
|
|
242
|
+
## Производительность
|
|
243
|
+
|
|
244
|
+
Список рендерится построчно: в DOM находятся только строки из окна
|
|
245
|
+
виртуализации плюс несколько строк запаса. При скролле диапазон
|
|
246
|
+
пересчитывается в `requestAnimationFrame`, и React получает новое состояние
|
|
247
|
+
только если окно действительно сдвинулось. Заголовки секций прилипают к
|
|
248
|
+
верхней границе, а клик по вкладке подсвечивает её сразу и доезжает до секции
|
|
249
|
+
короткой анимацией.
|
|
250
|
+
|
|
251
|
+
Поиск выполняется по массиву эмодзи в памяти (1914 записей на локаль), поэтому
|
|
252
|
+
выдача обновляется на каждое нажатие клавиши без задержек.
|
|
253
|
+
|
|
254
|
+
## Лицензия
|
|
255
|
+
|
|
256
|
+
Код — MIT. Названия и теги эмодзи происходят из
|
|
257
|
+
[emojibase](https://github.com/milesj/emojibase) (MIT) и аннотаций Unicode CLDR
|
|
258
|
+
(Unicode License v3); подробности — в `THIRD_PARTY_NOTICES.md`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Сторонние данные и лицензии
|
|
2
|
+
|
|
3
|
+
## emojibase-data
|
|
4
|
+
|
|
5
|
+
Файлы `data/ru.json` и `data/en.json` сгенерированы из пакета
|
|
6
|
+
[`emojibase-data`](https://github.com/milesj/emojibase) (лицензия MIT) скриптом
|
|
7
|
+
`scripts/build-data.ts`.
|
|
8
|
+
|
|
9
|
+
## Unicode CLDR
|
|
10
|
+
|
|
11
|
+
Названия и теги эмодзи в `emojibase-data` основаны на аннотациях
|
|
12
|
+
[Unicode CLDR](https://cldr.unicode.org/), которые распространяются по
|
|
13
|
+
[Unicode License v3](https://www.unicode.org/license.txt).
|
|
14
|
+
|
|
15
|
+
## Шрифт эмодзи
|
|
16
|
+
|
|
17
|
+
Пикер не поставляет изображений юникод-эмодзи и рендерит их системным шрифтом.
|
|
18
|
+
Список семейств задаётся переменной `--ge-font-emoji`.
|