[0-9a-f]+(?: [0-9a-f]+)*) +; +(?[a-z-]+) +# (?[^ ]+) E(?[\d.]+) (?.+)$/i)?.groups;
373 | if (m) {
374 | if (m['group']) {
375 | g = {name: m['group'], sub: []};
376 | groups.push(g);
377 | } else if (m['subGroup']) {
378 | s = {name: m['subGroup'], clusters: []};
379 | g.sub.push(s);
380 | } else if (m['code']) {
381 | const sequence = String.fromCodePoint(...m['code'].split(' ').map((hex) => parseInt(hex, 16)));
382 | s.clusters.push(sequence);
383 | sequences[sequence] = {
384 | sequence,
385 | type: m['type'] as SequenceType,
386 | name: m['name'],
387 | version: parseFloat(m['version']),
388 | };
389 | }
390 | }
391 | }
392 |
393 | return {groups, sequences};
394 | }
395 |
396 | export type NamedSequencesData = {
397 | cluster: string;
398 | name: string;
399 | }[];
400 |
401 | export async function parseNamedSequences(resources: UnicodeResources): Promise {
402 | const data = await resources.namedSequences();
403 | const namedSequences: NamedSequencesData = [];
404 | for (const line of data.split('\n')) {
405 | if (!line.length || line.startsWith('#')) continue;
406 | const m = line.match(/^(?[^#;]+[^#;\s])\s*;\s*(?[0-9A-F][0-9A-F ]+[0-9A-F])\s*(?:#|$)/)?.groups;
407 | if (m) {
408 | const cluster = String.fromCodePoint(...m['code'].split(/\s+/).map(c => parseInt(c, 16)));
409 | namedSequences.push({
410 | cluster,
411 | name: m['name']
412 | });
413 | } else console.warn('[parseNamedSequences] failed to parse', line);
414 | }
415 | return namedSequences;
416 | }
417 |
418 | export type AnnotationsData = {
419 | identity: {
420 | version: {
421 | _cldrVersion: string;
422 | };
423 | language: string;
424 | };
425 | annotations: Record;
429 | }
430 |
431 | export async function parseAnnotations(resources: UnicodeResources): Promise {
432 | const data = await (resources.annotations()).then(f => JSON.parse(f));
433 | return (data.annotations) as AnnotationsData;
434 | }
435 |
--------------------------------------------------------------------------------
/wwwassets/script/chars.ts:
--------------------------------------------------------------------------------
1 | export const SoftHyphen = "\u00ad";
2 | export const ZeroWidthJoiner = '\u200D';
3 |
4 | /** Variation selector 15: text-style for emoji sequences */
5 | export const VarSel15 = '\uFE0E';
6 | /** Variation selector 16: emoji-style for emoji sequences */
7 | export const VarSel16 = '\uFE0E';
8 |
9 | /** Used to terminate flag sequences */
10 | export const CancelTag = '\uDB40\uDC7F';
11 | /** Used in subdivision flag sequences */
12 | export const TagLatinSmallLetterA = '\uDB40\uDC61';
13 | export const TagLatinSmallLetterACode = TagLatinSmallLetterA.codePointAt(0)!;
14 | /** Used in subdivision flag sequences */
15 | export const TagLatinSmallLetterZ = '\uDB40\uDC7A';
16 | export const TagLatinSmallLetterZCode = TagLatinSmallLetterZ.codePointAt(0)!;
17 | /** Used in country flag sequences */
18 | export const RegionalIndicatorSymbolLetterA = "🇦";
19 | export const RegionalIndicatorSymbolLetterACode = RegionalIndicatorSymbolLetterA.codePointAt(0)!;
20 | /** Used in country flag sequences */
21 | export const RegionalIndicatorSymbolLetterZ = "🇿";
22 | export const RegionalIndicatorSymbolLetterZCode = RegionalIndicatorSymbolLetterZ.codePointAt(0)!;
--------------------------------------------------------------------------------
/wwwassets/script/config.tsx:
--------------------------------------------------------------------------------
1 | import {Fragment, h} from "preact";
2 | import {DigitsRow, FirstRow, Layout, SecondRow} from "./layout";
3 | import {ConfigActionKey, ConfigBuildKey, ConfigLabelKey, ConfigToggleKey, ExitSearchKey, PageKey} from "./key";
4 | import {Board} from "./board";
5 | import {useContext} from "preact/hooks";
6 | import {app, ConfigContext, LayoutContext} from "./appVar";
7 | import {SC} from "./layout/sc";
8 | import {BoardState, Keys, mapKeysToSlots, SlottedKeys} from "./boards/utils";
9 | import {KeyName} from "./keys/base";
10 | import {ahkOpenLink, ahkReload} from "./ahk";
11 |
12 | // Backslash and Enter appears on the first or the second row resp. second or both, so they're listed in both
13 | // noinspection JSUnusedLocalSymbols
14 | const SHORTCUT_KEYS = [
15 | {
16 | name: "Function Row", keys: [
17 | SC.Esc,
18 | SC.F1, SC.F2, SC.F3, SC.F4, SC.F5, SC.F6, SC.F7, SC.F8, SC.F9, SC.F10, SC.F11, SC.F12,
19 | ]
20 | }, {
21 | name: "Numbers Row", keys: [
22 | SC.Backtick,
23 | SC.Digit1, SC.Digit2, SC.Digit3, SC.Digit4, SC.Digit5, SC.Digit6, SC.Digit7, SC.Digit8, SC.Digit9, SC.Digit0,
24 | SC.Minus, SC.Equal,
25 | SC.Backspace,
26 | ]
27 | }, {
28 | name: "First Row", keys: [
29 | SC.Tab,
30 | SC.Q, SC.W, SC.E, SC.R, SC.T, SC.Y, SC.U, SC.I, SC.O, SC.P,
31 | SC.LeftBrace, SC.RightBrace, SC.Backslash, SC.Enter,
32 | ]
33 | }, {
34 | name: "Second Row", keys: [
35 | SC.CapsLock,
36 | SC.A, SC.S, SC.D, SC.F, SC.G, SC.H, SC.J, SC.K, SC.L,
37 | SC.Semicolon, SC.Apostrophe, SC.Backslash, SC.Enter,
38 | ]
39 | }, {
40 | name: "Third Row", keys: [
41 | SC.LessThan,
42 | SC.Z, SC.X, SC.C, SC.V, SC.B, SC.N, SC.M,
43 | SC.Comma, SC.Period, SC.Slash,
44 | ]
45 | }, {
46 | name: "Fourth Row", keys: [
47 | SC.Space
48 | ]
49 | }, {
50 | name: "Numpad", keys: [
51 | SC.Num0, SC.Num1, SC.Num2, SC.Num3,
52 | SC.Num4, SC.Num5, SC.Num6,
53 | SC.Num7, SC.Num8, SC.Num9,
54 | SC.NumDecimal,
55 | SC.NumDiv,
56 | SC.NumMult,
57 | SC.NumSub,
58 | SC.NumAdd,
59 | SC.NumLock,
60 | ]
61 | }
62 | ] as const;
63 |
64 | const SkinTones = [
65 | {name: "neutral", symbol: "👤"},
66 | {name: "light", symbol: "🏻"},
67 | {name: "medium-light", symbol: "🏼"},
68 | {name: "medium", symbol: "🏽"},
69 | {name: "medium-dark", symbol: "🏾"},
70 | {name: "dark", symbol: "🏿"},
71 | ] as const;
72 |
73 | export type SkinTone = keyof typeof SkinTones;
74 |
75 | export const DefaultTheme = "material";
76 | export const Themes: { name: string, url: string, symbol: string }[] = [
77 | {name: DefaultTheme, url: "style/material.css", symbol: "🔳"},
78 | {name: "legacy", url: "style/legacy.css", symbol: "⬜"},
79 | ];
80 | type ThemeMode = "light" | "dark" | "system";
81 | type OpenAt = "last-position" | "bottom" | "text-caret" | "mouse";
82 |
83 | export interface RecentEmoji {
84 | symbol: string;
85 | useCount: number;
86 | }
87 |
88 | export interface AppConfig {
89 | isoKeyboard: boolean; // has an additional key next to left shift
90 | theme: string;
91 | themeMode: ThemeMode;
92 | x: number;
93 | y: number;
94 | width: number;
95 | height: number;
96 | devTools: boolean;
97 | openAt: OpenAt;
98 | opacity: number;
99 | recent: RecentEmoji[];
100 | skinTone: SkinTone;
101 | preferredVariant: Record;
102 | hideAfterInput: boolean;
103 | mainAfterInput: boolean;
104 | showAliases: boolean;
105 | showCharCodes: boolean;
106 | }
107 |
108 | export const DefaultOpacity = .90;
109 |
110 | export const DefaultConfig: AppConfig = {
111 | isoKeyboard: false,
112 | theme: DefaultTheme,
113 | themeMode: "system",
114 | x: -1,
115 | y: -1,
116 | width: 764,
117 | height: 240,
118 | devTools: false,
119 | openAt: "bottom",
120 | opacity: DefaultOpacity,
121 | skinTone: 0,
122 | preferredVariant: {},
123 | recent: [],
124 | hideAfterInput: false,
125 | mainAfterInput: false,
126 | showAliases: false,
127 | showCharCodes: false,
128 | };
129 |
130 | export const ThemesMap = new Map(Themes.map((t) => [t.name, t]));
131 | export const DefaultThemeUrl = ThemesMap.get(DefaultTheme)!!.url;
132 |
133 | export interface ConfigPage {
134 | name: string;
135 | symbol: string;
136 |
137 | keys(config: AppConfig, l: Layout): SlottedKeys;
138 | }
139 |
140 | const ConfigPages: ConfigPage[] = [
141 | {
142 | name: "General",
143 | symbol: "🛠️",
144 | keys(config: AppConfig, l) {
145 | return {
146 | [SC.Q]: new ConfigToggleKey({
147 | active: config.isoKeyboard,
148 | statusName: `ISO layout: ${config.isoKeyboard ? 'on' : 'off'}`,
149 | action() {
150 | app().updateConfig({isoKeyboard: !config.isoKeyboard});
151 | }
152 | }),
153 | [SC.W]: new ConfigLabelKey(ISO layout ( between and ) ),
155 | ...mapKeysToSlots(l.freeRows[2], [
156 | // ...SkinTones.map((s, i) => new ConfigActionKey({
157 | // active: i == config.skinTone,
158 | // action() {
159 | // p.app.updateConfig({skinTone: i});
160 | // },
161 | // name: s.name,
162 | // symbol: s.symbol
163 | // })),
164 | new ConfigActionKey({
165 | action() {
166 | app().updateConfig({preferredVariant: {}})
167 | },
168 | active: !Object.keys(config.preferredVariant).length,
169 | name: "Reset Variants",
170 | statusName: "Clear variant preferences for all symbols",
171 | symbol: "🥷"
172 | })
173 | // new ConfigLabelKey("Default skin tone")
174 | ]),
175 | ...mapKeysToSlots(l.freeRows[3], [
176 | new ConfigActionKey({
177 | name: 'Fallback Fonts',
178 | symbol: '🔣',
179 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/wiki/Fallback-Fonts'),
180 | }),
181 | new ConfigActionKey({
182 | name: 'Font Folder',
183 | symbol: '📂',
184 | action: () => ahkOpenLink('.\\wwwassets\\fallback_fonts'),
185 | }),
186 | new ConfigActionKey({
187 | name: 'Plugins',
188 | symbol: '🪇',
189 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/wiki/Plugins'),
190 | }),
191 | new ConfigActionKey({
192 | name: 'Plugins Folder',
193 | symbol: '📂',
194 | action: () => ahkOpenLink('.\\wwwassets\\plugins'),
195 | }),
196 | ]),
197 | }
198 | }
199 | },
200 | {
201 | name: "Theme",
202 | symbol: "🎨",
203 | keys(config: AppConfig) {
204 | return {
205 | ...mapKeysToSlots(FirstRow, Themes.map((t) => new ConfigActionKey({
206 | active: config.theme == t.name,
207 | name: t.name, statusName: `Theme: ${t.name}`,
208 | symbol: t.symbol,
209 | action() {
210 | app().updateConfig({theme: t.name});
211 | }
212 | }))),
213 | ...mapKeysToSlots(SecondRow, [
214 | ...([
215 | ["light", "🌞"],
216 | ["dark", "🌚"],
217 | ["system", "📟"]
218 | ] as const).map(([mode, symbol]) => new ConfigActionKey({
219 | active: config.themeMode == mode,
220 | name: mode, symbol: symbol,
221 | statusName: `Theme variant: ${mode}`,
222 | action() {
223 | app().updateConfig({themeMode: mode})
224 | }
225 | }))
226 | ])
227 | }
228 | }
229 | },
230 | {
231 | name: "Display",
232 | symbol: "🖥️",
233 | keys(config, l) {
234 | return {
235 | ...mapKeysToSlots(l.freeRows[1], [
236 | new ConfigActionKey({
237 | symbol: "⏬", name: "-10", statusName: 'Opacity -10', action() {
238 | app().updateConfig({opacity: Math.max(config.opacity - .1, .2)})
239 | }
240 | }),
241 | new ConfigActionKey({
242 | symbol: "🔽", name: "-1", statusName: 'Opacity -1', action() {
243 | app().updateConfig({opacity: Math.max(config.opacity - .01, .2)})
244 | }
245 | }),
246 | new ConfigActionKey({
247 | symbol: "🔼", name: "+1", statusName: 'Opacity +1', action() {
248 | app().updateConfig({opacity: Math.min(config.opacity + .01, 1)})
249 | }
250 | }),
251 | new ConfigActionKey({
252 | symbol: "⏫", name: "+10", statusName: 'Opacity +10', action() {
253 | app().updateConfig({opacity: Math.min(config.opacity + .1, 1)})
254 | }
255 | }),
256 | new ConfigActionKey({
257 | symbol: `${Math.round(config.opacity * 100)}`,
258 | name: "reset",
259 | statusName: 'Reset opacity',
260 | action() {
261 | app().updateConfig({opacity: DefaultOpacity})
262 | }
263 | }),
264 | new ConfigLabelKey("Opacity")
265 | ]),
266 | ...mapKeysToSlots(l.freeRows[2], [
267 | ...([
268 | ["last position", "🗿", "last-position"],
269 | ["bottom", "👇", "bottom"],
270 | ["caret (beta)", "⌨️", "text-caret"],
271 | ["mouse", "🖱️", "mouse"]
272 | ] as const).map(([name, symbol, mode]) => new ConfigActionKey({
273 | active: config.openAt == mode,
274 | name, statusName: `Open at ${name}`, symbol,
275 | action() {
276 | app().updateConfig({openAt: mode})
277 | }
278 | })),
279 | new ConfigLabelKey("Open At")
280 | ]),
281 | ...mapKeysToSlots(l.freeRows[3], [
282 | new ConfigToggleKey({
283 | active: config.hideAfterInput,
284 | name: 'Hide', statusName: `Hide after input: ${config.hideAfterInput ? 'on' : 'off'}`,
285 | symbol: '🫥',
286 | action() {
287 | app().updateConfig({hideAfterInput: !config.hideAfterInput})
288 | }
289 | }),
290 | new ConfigToggleKey({
291 | active: config.mainAfterInput,
292 | name: 'Go Home',
293 | symbol: '🏠', statusName: `Go home after input: ${config.hideAfterInput ? 'on' : 'off'}`,
294 | action() {
295 | app().updateConfig({mainAfterInput: !config.mainAfterInput})
296 | }
297 | }),
298 | new ConfigLabelKey('After Input')
299 | ])
300 | };
301 | }
302 | },
303 | {
304 | name: "Tools",
305 | symbol: "🔨",
306 | keys(config: AppConfig) {
307 | return {
308 | ...mapKeysToSlots(FirstRow, [
309 | new ConfigBuildKey(),
310 | new ConfigActionKey({
311 | action: ahkReload,
312 | name: 'Reload',
313 | symbol: '🔄'
314 | }),
315 | ]),
316 | ...mapKeysToSlots(SecondRow, [
317 | new ConfigToggleKey({
318 | active: config.devTools, statusName: `Open DevTools: ${config.devTools ? 'on' : 'off'}`,
319 | action() {
320 | app().updateConfig({devTools: !config.devTools});
321 | }
322 | }),
323 | new ConfigLabelKey("Open DevTools")
324 | ]),
325 | }
326 | }
327 | },
328 | {
329 | name: "Details",
330 | symbol: "🔬",
331 | keys(config: AppConfig) {
332 | return {
333 | ...mapKeysToSlots(FirstRow, [
334 | new ConfigToggleKey({
335 | active: config.showAliases,
336 | statusName: `Show aliases in status bar: ${config.showAliases ? 'on' : 'off'}`,
337 | action: () => app().updateConfig({showAliases: !config.showAliases}),
338 | name: 'Aliases',
339 | symbol: '📛',
340 | }),
341 | new ConfigToggleKey({
342 | active: config.showCharCodes,
343 | statusName: `Show char codes in status bar: ${config.showCharCodes ? 'on' : 'off'}`,
344 | action: () => app().updateConfig({showCharCodes: !config.showCharCodes}),
345 | name: 'Codes',
346 | symbol: '🔢',
347 | }),
348 | new ConfigLabelKey('in Status Bar')
349 | ]),
350 | }
351 | }
352 | },
353 | {
354 | name: "About",
355 | symbol: "📜",
356 | keys(config: AppConfig) {
357 | return {
358 | ...mapKeysToSlots(FirstRow, [
359 | new ConfigActionKey({
360 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard'),
361 | name: 'GitHub',
362 | symbol: '🐙'
363 | }),
364 | new ConfigActionKey({
365 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/blob/master/LICENSE'),
366 | name: 'MIT License',
367 | symbol: '⚖️'
368 | }),
369 | new ConfigActionKey({
370 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard#dependencies'),
371 | name: 'Deps',
372 | statusName: 'Dependencies',
373 | symbol: '🪃',
374 | }),
375 | new ConfigLabelKey('Emoji Keyboard by Gilles Waeber')
376 | ])
377 | }
378 | }
379 | }
380 | ];
381 |
382 | export class ConfigBoard extends Board {
383 | constructor() {
384 | super({name: '__config', symbol: '🛠️'});
385 | }
386 |
387 | Contents = ({state}: { state: BoardState | undefined }) => {
388 | const page = Math.min(state?.page ?? 0, ConfigPages.length - 1);
389 | const config = useContext(ConfigContext);
390 | const l = useContext(LayoutContext);
391 | const pageKeys = ConfigPages.map((c, n) => new PageKey(n, n === page, c.name, c.symbol));
392 | const keys = {
393 | [SC.Backtick]: new ExitSearchKey(),
394 | ...mapKeysToSlots(DigitsRow, pageKeys),
395 | ...ConfigPages[page].keys(config, l),
396 | }
397 | return
398 | }
399 | }
400 |
--------------------------------------------------------------------------------
/wwwassets/script/config/boards.ts:
--------------------------------------------------------------------------------
1 | import {SoftHyphen, ZeroWidthJoiner} from "../chars";
2 | import {VK, VKAbbr} from "../layout/vk";
3 | import {ArrowsKeyboard} from "./arrows";
4 | import {MathKeyboard} from "./math";
5 | import {emojiGroup} from "../unicodeInterface";
6 | import {UnicodeKeyboard} from "./unicodeBoard";
7 | import {toCodePoints} from "../builder/builder";
8 | import {ExtendedLatin} from "./extendedLatin";
9 |
10 | function unicodeRange(from: string | number, to: string | number): string[] {
11 | const result: string[] = [];
12 | const codeFrom = typeof from === 'number' ? from : toCodePoints(from)[0];
13 | const codeTo = typeof to === 'number' ? to : toCodePoints(to)[0];
14 | if (codeFrom && codeTo && codeTo > codeFrom) {
15 | for (let i = codeFrom; i <= codeTo; ++i) result.push(String.fromCodePoint(i));
16 | }
17 | return result;
18 | }
19 |
20 | /**
21 | * An item that will be placed on one key, either:
22 | * - a symbol
23 | * - a blank key
24 | * - array of 1 symbol: symbol without variants (atm. only emoji skin color)
25 | * - array of 2 symbols: the second symbols is accessed with shift or right-click, e.g. for small and capital letters
26 | * - array of 3+ symbols: least-recently used symbol on left click, other symbols on right click
27 | * - a keyboard key (can be nested)
28 | */
29 | export type KeyboardItem = string | null | string[] | EmojiKeyboard | Cluster;
30 | export type SpriteRef = {
31 | spriteMap: string;
32 | sprite: string;
33 | };
34 | export type KeyCap = string | SpriteRef;
35 | export type EmojiKeyboard = {
36 | /** Name must be unique */
37 | name: string;
38 | /** Name as shown in the status bar */
39 | statusName?: string;
40 | symbol: KeyCap;
41 | /** Only set to true on the main keyboard */
42 | top?: true;
43 | /** Do not add to recently used */
44 | noRecent?: true;
45 | /** Place the items on the free keys, paging when necessary */
46 | content?: KeyboardItem[] | (() => KeyboardItem[]);
47 | /** Place the items according the Virtual Key code i.e. based on the symbols on the keys */
48 | byVK?: { [vk in VK | VKAbbr]?: KeyboardItem }
49 | /** Place the items by row */
50 | byRow?: (KeyboardItem[] | Record)[]
51 | };
52 | export type Cluster = {
53 | cluster: string;
54 | name: string;
55 | symbol?: KeyCap;
56 | };
57 |
58 | export function isCluster(item: KeyboardItem): item is Cluster {
59 | return typeof item === 'object' && (item?.hasOwnProperty('cluster') ?? false);
60 | }
61 |
62 | export type SpriteMap = {
63 | path: string,
64 | width: number,
65 | height: number,
66 | padding?: number;
67 | cols: number;
68 | rows: number;
69 | index: Record
70 | }
71 | export type Plugin = {
72 | name: string;
73 | data: PluginData;
74 | }
75 | export type PluginData = {
76 | name: string;
77 | symbol: KeyCap;
78 | boards?: EmojiKeyboard[];
79 | spriteMaps?: Record;
80 | }
81 |
82 | export const MAIN_BOARD: EmojiKeyboard = {
83 | name: 'Main Board',
84 | top: true,
85 | symbol: '⌨',
86 | content: [
87 | {
88 | name: "Happy",
89 | symbol: "😀",
90 | content: [
91 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-smiling"}),
92 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-affection"}),
93 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-tongue"}),
94 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-hand"}),
95 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-neutral-skeptical"}),
96 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-hat"}),
97 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-glasses"}),
98 | ]
99 | },
100 | {
101 | name: "Unwell",
102 | symbol: "😱",
103 | content: [
104 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-sleepy"}),
105 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-unwell"}),
106 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-concerned"}),
107 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-negative"}),
108 | ]
109 | },
110 | {
111 | name: "Roles",
112 | symbol: "👻",
113 | content: [
114 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-costume"}),
115 | ...emojiGroup({group: "People & Body", subGroup: "person-fantasy"}),
116 | ]
117 | },
118 | {
119 | name: "Body",
120 | symbol: "👍",
121 | content: [
122 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-open"}),
123 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-partial"}),
124 | ...emojiGroup({group: "People & Body", subGroup: "hand-single-finger"}),
125 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-closed"}),
126 | ...emojiGroup({group: "People & Body", subGroup: "hands"}),
127 | ...emojiGroup({group: "People & Body", subGroup: "hand-prop"}),
128 | ...emojiGroup({group: "People & Body", subGroup: "body-parts"}),
129 | ]
130 | },
131 | {
132 | name: "Gestures & activities",
133 | symbol: "💃",
134 | content: [
135 | ...emojiGroup({group: "People & Body", subGroup: "person-gesture"}),
136 | ...emojiGroup({group: "People & Body", subGroup: "person-activity"}),
137 | ...emojiGroup({group: "People & Body", subGroup: "person-resting"}),
138 | ]
139 | },
140 | {
141 | name: "Persons",
142 | symbol: "👤",
143 | content: [
144 | ...emojiGroup({group: "People & Body", subGroup: "person"}),
145 | ...emojiGroup({group: "People & Body", subGroup: "person-role"}),
146 | ...emojiGroup({group: "People & Body", subGroup: "person-symbol"}),
147 | ]
148 | },
149 | {
150 | name: "Emotions",
151 | symbol: "😺",
152 | content: [
153 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "cat-face"}),
154 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "monkey-face"}),
155 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "heart"}),
156 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "emotion"}),
157 | ]
158 | },
159 | {
160 | name: "Families",
161 | symbol: "👪",
162 | content: [
163 | ...emojiGroup({group: "People & Body", subGroup: "family"}),
164 | ]
165 | },
166 | {
167 | name: "Clothing",
168 | symbol: "👖",
169 | content: [
170 | ...emojiGroup({group: "Objects", subGroup: "clothing"}),
171 | ]
172 | },
173 | {
174 | name: "Animals",
175 | symbol: "🐦",
176 | content: [
177 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-mammal"}),
178 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-bird"}),
179 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-amphibian"}),
180 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-reptile"}),
181 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-marine"}),
182 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-bug"}),
183 | ]
184 | },
185 | {
186 | name: "Plants",
187 | symbol: "🌹",
188 | content: [
189 | ...emojiGroup({group: "Animals & Nature", subGroup: "plant-flower"}),
190 | ...emojiGroup({group: "Animals & Nature", subGroup: "plant-other"}),
191 | ]
192 | },
193 | {
194 | name: "Raw food",
195 | symbol: "🥝",
196 | content: [
197 | ...emojiGroup({group: "Food & Drink", subGroup: "food-fruit"}),
198 | ...emojiGroup({group: "Food & Drink", subGroup: "food-vegetable"}),
199 | ...emojiGroup({group: "Food & Drink", subGroup: "drink"}),
200 | ...emojiGroup({group: "Food & Drink", subGroup: "dishware"}),
201 | ]
202 | },
203 | {
204 | name: "Cooked",
205 | symbol: "🌭",
206 | content: [
207 | ...emojiGroup({group: "Food & Drink", subGroup: "food-prepared"}),
208 | ...emojiGroup({group: "Food & Drink", subGroup: "food-asian"}),
209 | ...emojiGroup({group: "Food & Drink", subGroup: "food-sweet"}),
210 | ]
211 | },
212 | {
213 | name: "Places",
214 | symbol: "🏡",
215 | content: [
216 | ...emojiGroup({group: "Travel & Places", subGroup: "place-map"}),
217 | ...emojiGroup({group: "Travel & Places", subGroup: "place-geographic"}),
218 | ...emojiGroup({group: "Travel & Places", subGroup: "place-building"}),
219 | ...emojiGroup({group: "Travel & Places", subGroup: "place-religious"}),
220 | ...emojiGroup({group: "Travel & Places", subGroup: "place-other"}),
221 | ...emojiGroup({group: "Travel & Places", subGroup: "hotel"}),
222 | ]
223 | },
224 | {
225 | name: "Vehicles",
226 | symbol: "🚗",
227 | content: [
228 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-ground"}),
229 | ]
230 | },
231 | {
232 | name: "Ships",
233 | symbol: "✈️",
234 | content: [
235 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-air"}),
236 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-water"}),
237 | ]
238 | },
239 | {
240 | name: "Time",
241 | symbol: "⌛",
242 | content: [
243 | ...emojiGroup({group: "Travel & Places", subGroup: "time"})
244 | ]
245 | },
246 | {
247 | name: "Weather",
248 | symbol: "⛅",
249 | content: [
250 | ...emojiGroup({group: "Travel & Places", subGroup: "sky & weather"})
251 | ]
252 | },
253 | {
254 | name: "Sports",
255 | symbol: "🎽",
256 | content: [
257 | ...emojiGroup({group: "Activities", subGroup: "sport"}),
258 | ...emojiGroup({group: "People & Body", subGroup: "person-sport"})
259 | ]
260 | },
261 | {
262 | name: "Activities",
263 | symbol: "🎮",
264 | content: [
265 | ...emojiGroup({group: "Activities", subGroup: "event"}),
266 | ...emojiGroup({group: "Activities", subGroup: "award-medal"}),
267 | ...emojiGroup({group: "Activities", subGroup: "game"}),
268 | ...emojiGroup({group: "Activities", subGroup: "arts & crafts"})
269 | ]
270 | },
271 | {
272 | name: "Sound & light",
273 | symbol: "🎥",
274 | content: [
275 | ...emojiGroup({group: "Objects", subGroup: "sound"}),
276 | ...emojiGroup({group: "Objects", subGroup: "music"}),
277 | ...emojiGroup({group: "Objects", subGroup: "musical-instrument"}),
278 | ...emojiGroup({group: "Objects", subGroup: "light & video"})
279 | ]
280 | },
281 | {
282 | name: "Tech",
283 | symbol: "💻",
284 | content: [
285 | ...emojiGroup({group: "Objects", subGroup: "phone"}),
286 | ...emojiGroup({group: "Objects", subGroup: "computer"}),
287 | ...emojiGroup({group: "Objects", subGroup: "mail"}),
288 | ]
289 | },
290 | {
291 | name: "Objects",
292 | symbol: "📜",
293 | content: [
294 | ...emojiGroup({group: "Objects", subGroup: "book-paper"}),
295 | ...emojiGroup({group: "Objects", subGroup: "money"}),
296 | ...emojiGroup({group: "Objects", subGroup: "writing"}),
297 | ...emojiGroup({group: "Objects", subGroup: "science"}),
298 | ...emojiGroup({group: "Objects", subGroup: "medical"}),
299 | ...emojiGroup({group: "Objects", subGroup: "household"}),
300 | ...emojiGroup({group: "Objects", subGroup: "other-object"}),
301 | ]
302 | },
303 | {
304 | name: "Work",
305 | symbol: "💼",
306 | content: [
307 | ...emojiGroup({group: "Objects", subGroup: "office"}),
308 | ...emojiGroup({group: "Objects", subGroup: "lock"}),
309 | ...emojiGroup({group: "Objects", subGroup: "tool"})
310 | ]
311 | },
312 | {
313 | name: "Signs",
314 | symbol: "⛔",
315 | content: [
316 | ...emojiGroup({group: "Symbols", subGroup: "transport-sign"}),
317 | ...emojiGroup({group: "Symbols", subGroup: "warning"}),
318 | ...emojiGroup({group: "Symbols", subGroup: "zodiac"}),
319 | ...emojiGroup({group: "Flags", subGroup: "flag"}),
320 | ]
321 | },
322 | {
323 | name: "Symbols",
324 | symbol: "⚜️",
325 | content: [
326 | ...emojiGroup({group: "Symbols", subGroup: "religion"}),
327 | ...emojiGroup({group: "Symbols", subGroup: "gender"}),
328 | ...emojiGroup({group: "Symbols", subGroup: "punctuation"}),
329 | ...emojiGroup({group: "Symbols", subGroup: "currency"}),
330 | ...emojiGroup({group: "Symbols", subGroup: "other-symbol"})
331 | ]
332 | },
333 | {
334 | name: "Alphanum",
335 | symbol: "🔤",
336 | content: [
337 | ...emojiGroup({group: "Symbols", subGroup: "alphanum"})
338 | ]
339 | },
340 | {
341 | name: "Geometric & keys",
342 | symbol: "🔷",
343 | content: [
344 | ...emojiGroup({group: "Symbols", subGroup: "keycap"}),
345 | ...emojiGroup({group: "Symbols", subGroup: "geometric"}),
346 | ...emojiGroup({group: "Symbols", subGroup: "av-symbol"})
347 | ]
348 | },
349 | {
350 | name: "World Flags",
351 | symbol: "🌐",
352 | content: [
353 | ...emojiGroup({group: "Flags", subGroup: "country-flag"}),
354 | ...emojiGroup({group: "Flags", subGroup: "subdivision-flag"}),
355 | ]
356 | },
357 | {
358 | name: "Greek",
359 | symbol: "π",
360 | noRecent: true,
361 | content: [
362 | "∂",
363 | "ϵ",
364 | "ϑ",
365 | "ϴ",
366 | "ϰ",
367 | "ϖ",
368 | "ϱ",
369 | "ϕ",
370 | "∇",
371 | ["ϝ", "Ϝ"],
372 | ],
373 | byVK: {
374 | a: ["α", "Α"],
375 | b: ["β", "Β"],
376 | c: ["ψ", "Ψ"],
377 | d: ["δ", "Δ"],
378 | e: ["ε", "Ε"],
379 | f: ["φ", "Φ"],
380 | g: ["γ", "Γ"],
381 | h: ["η", "Η"],
382 | i: ["ι", "Ι"],
383 | j: ["ξ", "Ξ"],
384 | k: ["κ", "Κ"],
385 | l: ["λ", "Λ"],
386 | m: ["μ", "Μ"],
387 | n: ["ν", "Ν"],
388 | o: ["ο", "Ο"],
389 | p: ["π", "Π"],
390 | q: ";",
391 | r: ["ρ", "Ρ"],
392 | s: ["σ", "Σ"],
393 | t: ["τ", "Τ"],
394 | u: ["θ", "Θ"],
395 | v: ["ω", "Ω"],
396 | w: "ς",
397 | x: ["χ", "Χ"],
398 | y: ["υ", "Υ"],
399 | z: ["ζ", "Ζ"],
400 | [VK.Period]: "·",
401 | [VK.Comma]: "ϐ",
402 | }
403 | },
404 | {
405 | name: "Boxes",
406 | symbol: "╚",
407 | byRow: [
408 | [
409 | ["┌", "╔"],
410 | ["┬", "╦"],
411 | ["┐", "╗"],
412 | ["┏"],
413 | ["┳"],
414 | ["┓"],
415 | ["╒", "╓"],
416 | ["╤", "╥"],
417 | ["╕", "╖"],
418 | "╭",
419 | "╮",
420 | {
421 | name: "Dashed",
422 | symbol: "┉",
423 | byRow: [
424 | ["┆", "┇", "┊", "┋", "╎", "╏"],
425 | ["┄", "┅", "┈", "┉", "╌", "╍"]
426 | ]
427 | }
428 | ],
429 | [
430 | ["├", "╠"],
431 | ["┼", "╬"],
432 | ["┤", "╣"],
433 | ["┣"],
434 | ["╋"],
435 | ["┫"],
436 | ["╞", "╟"],
437 | ["╪", "╫"],
438 | ["╡", "╢"],
439 | "╰",
440 | "╯",
441 | {
442 | name: "Mixed Lines",
443 | symbol: "╆",
444 | content: [
445 | "┍", "┎", "┭", "┮",
446 | "┯", "┰", "┱", "┲",
447 | "┑", "┒",
448 | "┝", "┞", "┟", "┠", "┡", "┢",
449 | "┽", "┾", "┿", "╀", "╁", "╂", "╃", "╄", "╅", "╆", "╇", "╈", "╉", "╊",
450 | "┥", "┦", "┧", "┨", "┩", "┪",
451 | "┕", "┖",
452 | "┵", "┶", "┷", "┸", "┹", "┺",
453 | "┙", "┚",
454 | "╽", "╿",
455 | "╼", "╾",
456 | ]
457 | }
458 | ],
459 | [
460 | ["└", "╚"],
461 | ["┴", "╩"],
462 | ["┘", "╝"],
463 | ["┗"],
464 | ["┻"],
465 | ["┛"],
466 | ["╘", "╙"],
467 | ["╧", "╨"],
468 | ["╛", "╜"],
469 | "╱",
470 | "╲",
471 | ],
472 | [
473 | ["│", "║"],
474 | ["─", "═"],
475 | "╳",
476 | ["┃"],
477 | ["━"],
478 | ["╴", "╸"],
479 | ["╵", "╹"],
480 | ["╷", "╻"],
481 | ["╶", "╺"],
482 | ]
483 | ],
484 | },
485 | ExtendedLatin,
486 | ArrowsKeyboard,
487 | {
488 | name: `Typo${SoftHyphen}graphy`,
489 | symbol: "‽",
490 | content: [
491 | "\u00a0", // No-Break\nSpace
492 | "\u202f", // Narrow\nNo-Break\nSpace
493 | "\u2001", // EM\nQuad
494 | "\u2000", // EN\nQuad
495 | "\u2003", // EM\nSpace
496 | "\u2002", // EN\nSpace
497 | "\u2004", // ⅓ EM\nSpace
498 | "\u2005", // ¼ EM\nSpace
499 | "\u2006", // ⅙ EM\nSpace
500 | "\u2009", // Thin\nSpace
501 | "\u200a", // Hair\nSpace
502 | "\u2007", // Figure\nSpace
503 | "\u2008", // Punctuation\nSpace
504 | "\u200b", // Zero\nWidth\nSpace
505 | "\u200c", // Zero\nWidth\nNon-Joiner
506 | ZeroWidthJoiner,
507 | "\u205f", // Medium\nMath\nSpace
508 | "\u3000", // Ideographic\nSpace
509 | SoftHyphen, // Soft\nHyphen
510 | "–", "—", "―", // Hyphens
511 | "“", "”", "‟", "„", "«", "»", "‹", "›", "‘", "’", "‛", "‚", // Quotes
512 | "¿", "¡", "‽", "‼", "°", "¦", // Punctuation
513 | "·",
514 | "•",
515 | ]
516 | },
517 | {
518 | name: "Currency",
519 | symbol: "¤",
520 | content: [
521 | "¤",
522 | /* Afghani */ "₳",
523 | /* Austral */ "؋",
524 | /* Baht */ "฿",
525 | /* Bitcoin */ "₿",
526 | /* Cedi */ "₵",
527 | /* Cent */ "¢",
528 | /* Colon */ "₡",
529 | /* Cruzeiro */ "₢",
530 | /* Dollar */ "$",
531 | /* Dong */ "₫",
532 | /* Drachma */ "₯",
533 | /* Euro, Currency */ "₠",
534 | /* Euro, Symbol */ "€",
535 | /* Floren */ "ƒ",
536 | /* Franc */ "₣",
537 | /* Guarani */ "₲",
538 | /* Hryvnia */ "₴",
539 | /* Kip */ "₭",
540 | /* Lari */ "₾",
541 | /* Lira */ "₤",
542 | /* Lira, Turkish */ "₺",
543 | /* Livre Tournois */ "₶",
544 | /* Manat */ "₼",
545 | /* Mark, German */ "ℳ",
546 | /* Mark, Nordic */ "₻",
547 | /* Mill */ "₥",
548 | /* Naira */ "₦",
549 | /* Penny, German */ "₰",
550 | /* Peseta */ "₧",
551 | /* Peso */ "₱",
552 | /* Pound */ "£",
553 | /* Rial */ "﷼",
554 | /* Riel, Khmer */ "៛",
555 | /* Ruble */ "₽",
556 | /* Rupee */ "₨",
557 | /* Rupee, Bengali */ "৳",
558 | /* Rupee, Gujarati */ "૱",
559 | /* Rupee, Indian */ "₹",
560 | /* Rupee, Mark */ "৲",
561 | /* Rupee, Tamil */ "௹",
562 | /* Sheqel, New */ "₪",
563 | /* Som */ "⃀",
564 | /* Spesmilo */ "₷",
565 | /* Tenge */ "₸",
566 | /* Tugrik */ "₮",
567 | /* Won */ "₩",
568 | /* Yen */ "¥",
569 | /* Yen 2 */ "円",
570 | /* Yuan */ "元",
571 | /* Yuan 2 */ "圓",
572 | ]
573 | },
574 | MathKeyboard,
575 | UnicodeKeyboard,
576 | ]
577 | };
578 |
--------------------------------------------------------------------------------
/wwwassets/script/config/fallback.ts:
--------------------------------------------------------------------------------
1 | import {Version} from "../osversion";
2 |
3 | export type IconRequirement = {
4 | type: "windows";
5 | /** Windows version in which the elements are supported */
6 | windows: string;
7 | } | {
8 | type: "zwjEmoji";
9 | /** Sequence with ZWJ to check: supported if the ZWJ sequence is shorter than the sequence with the ZWJs removed */
10 | zwjEmoji: string;
11 | } | {
12 | type: "emoji";
13 | /** Emoji to check: supported if the width is non-zero with a fallback on a zero-width font */
14 | emoji: string;
15 | };
16 | /** Fallback configuration: for each sequence we look for the first item matching and then use the fallback font if the conditions don't match. */
17 | export const IconFallback: {
18 | requirement: IconRequirement;
19 | /** Match when unicode version >= number */
20 | version?: Version;
21 | /** Match when emoji version >= number */
22 | emojiVersion?: number;
23 | /** Match for a given text range */
24 | ranges?: {
25 | from: string,
26 | to: string,
27 | }[],
28 | /** Match for given sequences or characters */
29 | clusters?: Set;
30 | }[] = [
31 | {
32 | // Not supported on Windows
33 | requirement: {type: "windows", windows: "99"},
34 | ranges: [
35 | {from: "🇦", to: "🇿🇿"},
36 | {
37 | from: "\ud83c\udff4\udb40\udc61", // WAVING BLACK FLAG, TAG LATIN SMALL LETTER A
38 | to: "\ud83c\udff4\udb40\udc7a\udb40\udc7a", // WAVING BLACK FLAG, TAG LATIN SMALL LETTER Z, TAG LATIN SMALL LETTER Z
39 | }
40 | ]
41 | },
42 | {
43 | requirement: {type: "emoji", emoji: ""}, // Harp
44 | version: new Version("16"),
45 | },
46 | {
47 | requirement: {type:"zwjEmoji", zwjEmoji: "🐦🔥"}, // Phoenix
48 | version: new Version("15.1"),
49 | },
50 | {
51 | requirement: {type:"zwjEmoji", zwjEmoji: "🐦⬛"}, // Black Bird
52 | version: new Version("15"),
53 | },
54 | {
55 | // Windows 11 Fluent emoji font
56 | requirement: {type:"zwjEmoji", zwjEmoji: "🫱🫲"}, // Handshake
57 | version: new Version("14"),
58 | },
59 | {
60 | // Windows 11 Preview
61 | requirement: {type: "windows", windows: "10.0.21277"},
62 | version: new Version("13"),
63 | clusters: new Set([
64 | "*️⃣",
65 | // Unicode 12.1: partial support
66 | "🧑🦰", "🧑🏻🦰", "🧑🏼🦰", "🧑🏽🦰", "🧑🏾🦰", "🧑🏿🦰", // Person Red Hair
67 | "🧑🦱", "🧑🏻🦱", "🧑🏼🦱", "🧑🏽🦱", "🧑🏾🦱", "🧑🏿🦱", // Person Curly Hair
68 | "🧑🦳", "🧑🏻🦳", "🧑🏼🦳", "🧑🏽🦳", "🧑🏾🦳", "🧑🏿🦳", // Person White Hair
69 | "🧑🦲", "🧑🏻🦲", "🧑🏼🦲", "🧑🏽🦲", "🧑🏾🦲", "🧑🏿🦲", // Person Bald
70 | "🧑⚕️", "🧑🏻⚕", "🧑🏼⚕", "🧑🏽⚕", "🧑🏾⚕", "🧑🏿⚕", // Health Worker
71 | "🧑🎓", "🧑🏻🎓", "🧑🏼🎓", "🧑🏽🎓", "🧑🏾🎓", "🧑🏿🎓", // Student
72 | "🧑🏫", "🧑🏻🏫", "🧑🏼🏫", "🧑🏽🏫", "🧑🏾🏫", "🧑🏿🏫", // Teacher
73 | "🧑⚖️", "🧑🏻⚖", "🧑🏼⚖", "🧑🏽⚖", "🧑🏾⚖", "🧑🏿⚖", // Judge
74 | "🧑🌾", "🧑🏻🌾", "🧑🏼🌾", "🧑🏽🌾", "🧑🏾🌾", "🧑🏿🌾", // Farmer
75 | "🧑🍳", "🧑🏻🍳", "🧑🏼🍳", "🧑🏽🍳", "🧑🏾🍳", "🧑🏿🍳", // Cook
76 | "🧑🔧", "🧑🏻🔧", "🧑🏼🔧", "🧑🏽🔧", "🧑🏾🔧", "🧑🏿🔧", // Mechanic
77 | "🧑🏭", "🧑🏻🏭", "🧑🏼🏭", "🧑🏽🏭", "🧑🏾🏭", "🧑🏿🏭", // Factory Worker
78 | "🧑💼", "🧑🏻💼", "🧑🏼💼", "🧑🏽💼", "🧑🏾💼", "🧑🏿💼", // Office Worker
79 | "🧑🔬", "🧑🏻🔬", "🧑🏼🔬", "🧑🏽🔬", "🧑🏾🔬", "🧑🏿🔬", // Scientist
80 | "🧑💻", "🧑🏻💻", "🧑🏼💻", "🧑🏽💻", "🧑🏾💻", "🧑🏿💻", // Technologist
81 | "🧑🎤", "🧑🏻🎤", "🧑🏼🎤", "🧑🏽🎤", "🧑🏾🎤", "🧑🏿🎤", // Singer
82 | "🧑🎨", "🧑🏻🎨", "🧑🏼🎨", "🧑🏽🎨", "🧑🏾🎨", "🧑🏿🎨", // Artist
83 | "🧑✈️", "🧑🏻✈", "🧑🏼✈", "🧑🏽✈", "🧑🏾✈", "🧑🏿✈", // Pilot
84 | "🧑🚀", "🧑🏻🚀", "🧑🏼🚀", "🧑🏽🚀", "🧑🏾🚀", "🧑🏿🚀", // Astronaut
85 | "🧑🚒", "🧑🏻🚒", "🧑🏼🚒", "🧑🏽🚒", "🧑🏾🚒", "🧑🏿🚒", // Firefighter
86 | "🧑🦯", "🧑🏻🦯", "🧑🏼🦯", "🧑🏽🦯", "🧑🏾🦯", "🧑🏿🦯", // Person with White Cane
87 | "🧑🦼", "🧑🏻🦼", "🧑🏼🦼", "🧑🏽🦼", "🧑🏾🦼", "🧑🏿🦼", // Person in Motorized Wheelchair
88 | "🧑🦽", "🧑🏻🦽", "🧑🏼🦽", "🧑🏽🦽", "🧑🏾🦽", "🧑🏿🦽", // Person in Manual Wheelchair
89 | ]),
90 | },
91 | {
92 | // Windows 19H1
93 | requirement: {type: "windows", windows: "10.0.18277"},
94 | version: new Version("12"),
95 | clusters: new Set([
96 | "🏴☠️",
97 | "#\ufe0f\u20e3",
98 | "0\ufe0f\u20e3",
99 | "1\ufe0f\u20e3",
100 | "2\ufe0f\u20e3",
101 | "3\ufe0f\u20e3",
102 | "4\ufe0f\u20e3",
103 | "5\ufe0f\u20e3",
104 | "6\ufe0f\u20e3",
105 | "7\ufe0f\u20e3",
106 | "8\ufe0f\u20e3",
107 | "9\ufe0f\u20e3"
108 | ])
109 | },
110 | {
111 | // Redstone 5
112 | requirement: {type: "windows", windows: "10.0.17723"},
113 | version: new Version("11"),
114 | },
115 | {
116 | // Windows Fall Creator Update
117 | requirement: {type: "windows", windows: "10.0.16226"},
118 | version: new Version("5"),
119 | // emojiVersion: 5,
120 | // version: 10
121 | },
122 | {
123 | // Windows Creator Update
124 | requirement: {type: "windows", windows: "10.0.15063"},
125 | version: new Version("4"),
126 | // emojiVersion: 4
127 | },
128 | {
129 | // Windows Anniversary Update
130 | requirement: {type: "windows", windows: "10.0.14393"},
131 | // version: 9
132 | },
133 | {
134 | requirement: {type: "windows", windows: "10"},
135 | version: new Version("0.01"),
136 | // version: 5
137 | },
138 | ];
139 |
--------------------------------------------------------------------------------
/wwwassets/script/config/unicodeBoard.ts:
--------------------------------------------------------------------------------
1 | import {charInfo, UnicodeData} from "../unicodeInterface";
2 | import {GeneralCategory} from "../builder/unicode";
3 | import {toHex} from "../builder/consolidated";
4 | import type {EmojiKeyboard} from "./boards";
5 |
6 | const BlockSymbolOverride: Record = {
7 | 0x2000: "†",
8 | 0x2800: "⠳",
9 | 0xFFF0: "�",
10 | 0x1D100: "𝄞",
11 | 0x1F300: "🌌",
12 | 0x1F900: "🤔",
13 | };
14 |
15 | function validCode(code: number): boolean {
16 | const info = charInfo(code);
17 | if (!info) return false;
18 | return !info.control && !info.reserved && !info.notACharacter;
19 | }
20 |
21 | export const UnicodeKeyboard: EmojiKeyboard = {
22 | name: "Unicode Blocks",
23 | symbol: "∪",
24 | content: UnicodeData.blocks.filter(
25 | b => b.sub.some(s => s.char?.some(validCode)) || b.isCJKUnifiedIdeographs
26 | ).map(block => {
27 | const codes = block.sub.map(sub => sub.char).flat().filter(validCode);
28 | let symbol = String.fromCodePoint(codes[0] ?? block.start);
29 | for (const code of codes) {
30 | const info = charInfo(code);
31 | if (info?.ca?.startsWith(GeneralCategory.Letter)) {
32 | symbol = String.fromCodePoint(code);
33 | break;
34 | }
35 | }
36 | if (BlockSymbolOverride[block.start]) symbol = BlockSymbolOverride[block.start];
37 | const statusName = `${block.name} ${toHex(block.start)}–${toHex(block.end)}`;
38 | return {
39 | name: block.name,
40 | statusName,
41 | symbol,
42 | content: () => {
43 | if (codes.length || !block.isCJKUnifiedIdeographs) {
44 | return codes.map(c => String.fromCodePoint(c));
45 | } else {
46 | const content = [];
47 | for (let i = block.start; i <= block.end; i++) {
48 | content.push(String.fromCodePoint(i));
49 | }
50 | return content;
51 | }
52 | },
53 | } as EmojiKeyboard;
54 | })
55 | };
56 |
--------------------------------------------------------------------------------
/wwwassets/script/emojis.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeData} from "./unicodeInterface";
2 | import {toCodePoints} from "./builder/builder";
3 | import {fromEntries} from "./helpers";
4 | import type {ExtendedClusterInformation} from "./builder/consolidated";
5 |
6 | const u = UnicodeData
7 |
8 | const ESCAPE_REGEX = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-'].join('|\\') + ')', 'g');
9 | const SEPARATOR = '%';
10 | const SEPARATOR_REGEX_G = /%/g;
11 | const SEARCH_ID = SEPARATOR + '([\\d,]+)' + SEPARATOR + '[^' + SEPARATOR + ']*';
12 |
13 | // Map preserves insertion order
14 | const index = new Map();
15 | (function () {
16 | /** named sequences to be put after emojis and unicode tables */
17 | const endClusters: ExtendedClusterInformation[] = [];
18 | for (const c of Object.values(u.clusters)) {
19 | if (!c.parent) {
20 | if (c.version) index.set(toCodePoints(c.cluster).join(','), `${c.name} ${c.alias?.join(' ') ?? ''}`);
21 | else endClusters.push(c);
22 | }
23 | }
24 | for (const c of Object.values(u.chars)) {
25 | if (!c.reserved && !c.notACharacter) {
26 | index.set(c.code.toString(), `${c.n} ${c.alias?.join(' ') ?? ''} ${c.falias?.join(' ') ?? ''}`);
27 | }
28 | }
29 | for (const c of endClusters) {
30 | index.set(toCodePoints(c.cluster).join(','), `${c.name} ${c.alias?.join(' ') ?? ''}`);
31 | }
32 | if (index.size) console.log("Unicode index built", index);
33 | })();
34 | const searchHaystack = Array.from(index.entries()).map(([k, v]) => `${SEPARATOR}${k}${SEPARATOR}${v.replace(SEPARATOR_REGEX_G, '')}`).join('');
35 |
36 | function escapeRegex(str: string): string {
37 | return str.replace(ESCAPE_REGEX, '\\$1');
38 | }
39 |
40 | export function search(needle: string): string[] {
41 | let result: string[] = [];
42 | needle.split(/\s+/g).forEach((n, k) => {
43 | let filter: string[] = [];
44 | if (!n.length) return;
45 | const re = new RegExp(SEARCH_ID + escapeRegex(n.replace(SEPARATOR_REGEX_G, '')), 'ig');
46 | for (let m; (m = re.exec(searchHaystack));) {
47 | filter.push(m[1]);
48 | }
49 | if (k == 0) result = filter;
50 | else {
51 | const f = new Set(filter);
52 | result = result.filter(v => f.has(v));
53 | }
54 | })
55 |
56 | // Score the entries, +1 for full word match, -1 for a match not at the start of a word
57 | const scores = fromEntries(result.map(v => [v, 0] as const));
58 | for (const n of needle.split(/\s+/g)) {
59 | if (!n.length) continue;
60 | const re1 = new RegExp('(?:[^a-z]|^)' + escapeRegex(n) + '(?:[^a-z]|$)', 'i');
61 | const re2 = new RegExp('(?:[^a-z]|^)' + escapeRegex(n), 'i');
62 | for (const r of result) {
63 | if (re1.test(index.get(r)!)) scores[r]++;
64 | else if (!re2.test(index.get(r)!)) scores[r]--;
65 | }
66 | }
67 | result.sort((a, b) => scores[b] - scores[a]);
68 | return result.map(v => String.fromCodePoint(...v.split(',').map(v => parseInt(v, 10))));
69 | }
70 |
--------------------------------------------------------------------------------
/wwwassets/script/entryPoint.ts:
--------------------------------------------------------------------------------
1 | import {startApp} from "./app";
2 | import {parseUnicodeResources, UnicodeResources} from "./builder/unicode";
3 | import memoizeOne from "memoize-one";
4 | import {ConsolidatedUnicodeData, consolidateUnicodeData} from "./builder/consolidated";
5 |
6 | if (typeof window != "undefined") {
7 | startApp();
8 | }
9 |
10 | export async function nodeBuild({ucdVersion, emojiVersion, cldrVersion}: {ucdVersion: string, emojiVersion: string, cldrVersion: string}): Promise {
11 | const resources: UnicodeResources = {
12 | emojiTest: memoizeOne(() => fetch(`https://unicode.org/Public/emoji/${emojiVersion}/emoji-test.txt`).then(r => r.text())),
13 | emojiData: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/emoji/emoji-data.txt`).then(r => r.text())),
14 | unicodeData: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/UnicodeData.txt`).then(r => r.text())),
15 | namesList: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/NamesList.txt`).then(r => r.text())),
16 | namedSequences: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/NamedSequences.txt`).then(r => r.text())),
17 | annotations: memoizeOne(() => fetch(`https://raw.githubusercontent.com/unicode-org/cldr-json/${cldrVersion}/cldr-json/cldr-annotations-full/annotations/en/annotations.json`).then(r => r.text())),
18 | }
19 |
20 | const ctx = await parseUnicodeResources(resources);
21 |
22 | return consolidateUnicodeData(ctx);
23 | }
24 |
--------------------------------------------------------------------------------
/wwwassets/script/helpers.ts:
--------------------------------------------------------------------------------
1 | export declare type Dictionary = { [key: string]: T };
2 |
3 | /** Mark as unreachable */
4 | export function unreachable(x: never): never {
5 | console.error("Unexpected", x);
6 | throw new Error("Unexpected: " + x);
7 | }
8 |
9 | /** CSS class builder, e.g. for React */
10 | export function cl(...args: (null|undefined|string|Dictionary)[]): string {
11 | return args.flatMap(a => {
12 | if (typeof a === "string") return a;
13 | if (a === null || a === undefined) return "";
14 | return Object.entries(a).map(([k, v]) => v ? k : "");
15 | }).join(' ');
16 | }
17 |
18 | type FromEntries = {
19 | [K in E[number][0]]: Extract[1]
20 | };
21 |
22 | /** Same as Object.fromEntries but properly typed */
23 | export function fromEntries(entries: T): FromEntries {
24 | const object: any = {};
25 | for (const [k, v] of entries) {
26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
27 | object[k] = v;
28 | }
29 | return object as FromEntries;
30 | }
31 |
--------------------------------------------------------------------------------
/wwwassets/script/key.tsx:
--------------------------------------------------------------------------------
1 | import {AppMode} from "./app";
2 | import {ComponentChild, h} from "preact";
3 | import {Board} from "./board";
4 | import {app, ConfigBuildingContext, ConfigContext} from "./appVar";
5 | import {cl} from "./helpers";
6 | import {clusterAliases, clusterName, clusterVariants} from "./unicodeInterface";
7 | import {makeBuild, toCodePoints} from "./builder/builder";
8 | import {useContext} from "preact/hooks";
9 | import {SC} from "./layout/sc";
10 | import {VarSel15} from "./chars";
11 | import {Key, KeyName} from "./keys/base";
12 | import {Symbol} from "./keys/symbol";
13 | import {AppConfig} from "./config";
14 | import {toHex} from "./builder/consolidated";
15 | import {KeyCap} from "./config/boards";
16 |
17 | export class ConfigKey extends Key {
18 | constructor() {
19 | super({name: "Settings", symbol: "🛠️" + VarSel15, clickAlwaysAlternate: true, keyNamePrefix: "⇧"});
20 | }
21 |
22 | actAlternate(): void {
23 | app().setMode(AppMode.SETTINGS);
24 | }
25 | }
26 |
27 | export class ConfigActionKey extends Key {
28 | protected action: () => void;
29 |
30 | constructor({action, ...p}: {
31 | active?: boolean,
32 | action(): void,
33 | name: string,
34 | statusName?: string,
35 | symbol: string
36 | }) {
37 | super(p);
38 | this.action = action;
39 | }
40 |
41 | act() {
42 | this.action();
43 | }
44 | }
45 |
46 | export class ConfigToggleKey extends ConfigActionKey {
47 | constructor({active, ...p}: {
48 | active?: boolean,
49 | action(): void,
50 | name?: string,
51 | statusName?: string,
52 | symbol?: string
53 | }) {
54 | active = active ?? false;
55 | super({
56 | name: active ? "On" : "Off",
57 | symbol: active ? "✔️" : "❌",
58 | active,
59 | ...p,
60 | });
61 | }
62 | }
63 |
64 | export class ConfigLabelKey extends Key {
65 | constructor(private text: ComponentChild) {
66 | super({name: "", symbol: ""});
67 | }
68 |
69 | Contents = ({code}: { code: SC }) => {
70 | return
71 |
72 | {this.text}
73 | ;
74 | }
75 | }
76 |
77 | export class ConfigBuildKey extends Key {
78 | constructor() {
79 | super({name: "Build", symbol: "🏗️"})
80 | }
81 |
82 | act() {
83 | makeBuild();
84 | }
85 |
86 | Contents = ({code}: { code: SC }) => {
87 | const active = useContext(ConfigBuildingContext);
88 | return {
91 | e.preventDefault();
92 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate() : this.act();
93 | }}
94 | onContextMenu={(e) => {
95 | e.preventDefault();
96 | this.actAlternate();
97 | }}
98 | onMouseOver={() => app().updateStatus(this.name)}
99 | >
100 |
101 | {this.name}
102 |
103 | ;
104 | }
105 | }
106 |
107 | export class BackKey extends Key {
108 | constructor() {
109 | super({name: 'Back/🛠️', symbol: '←', keyType: "back"});
110 | }
111 |
112 | act() {
113 | app().back();
114 | }
115 |
116 | actAlternate() {
117 | app().setMode(AppMode.SETTINGS);
118 | }
119 | }
120 |
121 | export class KeyboardKey extends Key {
122 | constructor(private target: Board) {
123 | super({name: target.name, statusName: target.statusName, symbol: target.symbol});
124 | }
125 |
126 | act() {
127 | app().setBoard(this.target);
128 | }
129 | }
130 |
131 | export class PageKey extends Key {
132 | constructor(private page: number, active: boolean, name?: string, symbol?: string) {
133 | super({name: name ?? `page ${page + 1}`, symbol: symbol ?? '' + (page + 1), active});
134 | }
135 |
136 | act() {
137 | if (!this.active) app().setPage(this.page);
138 | }
139 | }
140 |
141 | export class SearchKey extends Key {
142 | constructor() {
143 | super({name: 'search', symbol: '🔎' + VarSel15});
144 | }
145 |
146 | act() {
147 | app().setMode(AppMode.SEARCH);
148 | }
149 | }
150 |
151 | export class ExitSearchKey extends Key {
152 | constructor() {
153 | super({name: 'back', symbol: '←'});
154 | }
155 |
156 | act() {
157 | app().setMode(AppMode.MAIN);
158 | }
159 | }
160 |
161 | export class ClusterKey extends Key {
162 | private readonly variants: string[] | undefined;
163 | private readonly variantOf: string | undefined;
164 | private readonly noRecent: boolean;
165 | private readonly alt: boolean;
166 | private readonly lu: boolean;
167 | private readonly upperName: string;
168 |
169 | constructor(private cluster: string, p?: { variants?: string[], variantOf?: string, noRecent?: boolean, symbol?: KeyCap, name?: string }) {
170 | const name = p?.name ?? clusterName(cluster);
171 | const variants = p?.variants ?? clusterVariants(cluster);
172 | super({
173 | name,
174 | symbol: p?.symbol ?? cluster,
175 | keyType: "char",
176 | });
177 | this.alt = !!variants && variants.length > 2;
178 | this.lu = variants?.length === 2;
179 | this.upperName = variants?.length == 2 ? variants[1] : "";
180 | this.noRecent = p?.noRecent ?? false;
181 | this.variants = variants;
182 | this.variantOf = p?.variantOf;
183 | }
184 |
185 | act(config?: AppConfig) {
186 | if (this.alt) {
187 | config ??= app().getConfig();
188 | const cluster = config.preferredVariant[this.cluster] ?? this.cluster;
189 | app().send(cluster, {noRecent: this.noRecent, variantOf: this.variantOf});
190 | } else {
191 | app().send(this.cluster, {noRecent: this.noRecent, variantOf: this.variantOf});
192 | }
193 | }
194 |
195 | actAlternate(config?: AppConfig) {
196 | if (this.alt) {
197 | app().setBoard(Board.clusterAlternates(this.cluster, this.variants!, {noRecent: this.noRecent}));
198 | } else if (this.lu) {
199 | app().send(this.variants![1], {noRecent: this.noRecent});
200 | } else {
201 | app().send(this.cluster, {noRecent: this.noRecent}); // no variant change
202 | }
203 | }
204 |
205 | Contents = ({code}: { code: SC }) => {
206 | const config = useContext(ConfigContext);
207 | const cluster = this.alt ? config.preferredVariant[this.cluster] ?? this.cluster : this.cluster;
208 | const symbol = this.alt ? cluster : this.symbol;
209 | let status = this.alt ? clusterName(config.preferredVariant[this.cluster] ?? this.cluster) : this.name;
210 | if (config.showAliases) {
211 | const aliases = clusterAliases(this.cluster);
212 | if (aliases.length) status += ` (${aliases.join(', ')})`;
213 | }
214 | if (config.showCharCodes) {
215 | status += ` [${toCodePoints(cluster).map(toHex).join(' ')}]`
216 | }
217 | return {
220 | e.preventDefault();
221 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate(config) : this.act(config);
222 | }}
223 | onContextMenu={(e) => {
224 | e.preventDefault();
225 | this.actAlternate(config);
226 | }}
227 | onMouseOver={() => app().updateStatus(status)}
228 | data-keycode={code}
229 | >
230 | {this.keyNamePrefix}
231 | {this.upperName}
232 |
233 | ;
234 | }
235 | }
236 |
237 | export class RecentKey extends Key {
238 | constructor() {
239 | super({name: 'Recent', symbol: '↺'});
240 | }
241 |
242 | act() {
243 | app().setMode(AppMode.RECENTS);
244 | }
245 | }
246 |
247 | export class ExitRecentKey extends Key {
248 | constructor() {
249 | super({name: 'Back', symbol: '←', keyType: "back"});
250 | }
251 |
252 | act() {
253 | app().setMode(AppMode.MAIN);
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/wwwassets/script/keys/base.tsx:
--------------------------------------------------------------------------------
1 | import {SC} from "../layout/sc";
2 | import {useContext} from "preact/hooks";
3 | import {app, LayoutContext} from "../appVar";
4 | import {Fragment, h} from "preact";
5 | import {cl} from "../helpers";
6 | import {Symbol} from "./symbol";
7 | import {KeyCap} from "../config/boards";
8 |
9 | export function KeyName({code}: { code: SC }) {
10 | const layout = useContext(LayoutContext);
11 | return {layout.sys[code]?.name ?? ''} ;
12 | }
13 |
14 | export type KeyType = "action" | "empty" | "char" | "back";
15 |
16 | export class Key {
17 | public readonly name: string;
18 | /** Name used in the title bar */
19 | protected readonly statusName: string;
20 | public readonly symbol: KeyCap;
21 | public readonly active: boolean;
22 | public readonly blank: boolean;
23 | public readonly keyType: KeyType;
24 | protected readonly clickAlwaysAlternate: boolean;
25 | protected readonly keyNamePrefix: string;
26 |
27 |
28 | constructor(
29 | p: {
30 | name: string,
31 | statusName?: string,
32 | symbol: KeyCap,
33 | active?: boolean,
34 | clickAlwaysAlternate?: boolean,
35 | keyNamePrefix?: string,
36 | blank?: boolean,
37 | keyType?: KeyType
38 | }
39 | ) {
40 | this.name = p.name;
41 | this.statusName = p.statusName ?? p.name;
42 | this.symbol = p.symbol;
43 | this.active = p.active ?? false;
44 | this.clickAlwaysAlternate = p.clickAlwaysAlternate ?? false;
45 | this.keyNamePrefix = p.keyNamePrefix ?? '';
46 | this.blank = p.blank ?? false;
47 | this.keyType = !p.name.length ? "empty" : p.keyType ?? "action";
48 | }
49 |
50 | Contents = ({code}: { code: SC }) => {
51 | return {
54 | e.preventDefault();
55 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate() : this.act();
56 | }}
57 | onContextMenu={(e) => {
58 | e.preventDefault();
59 | this.actAlternate();
60 | }}
61 | onMouseOver={() => app().updateStatus(this.statusName)}
62 | data-keycode={code}
63 | >
64 | {this.keyNamePrefix}
65 | {this.name}
66 |
67 | ;
68 | }
69 |
70 | act(): void {
71 | // Default: do nothing
72 | }
73 |
74 | actAlternate(): void {
75 | this.act();
76 | }
77 | }
78 |
79 | export const BlankKey = new Key({name: '', symbol: '', blank: true, keyType: "empty"});
80 |
--------------------------------------------------------------------------------
/wwwassets/script/keys/symbol.tsx:
--------------------------------------------------------------------------------
1 | import {useContext, useMemo} from "preact/hooks";
2 | import {OSContext, PluginsContext} from "../appVar";
3 | import {charInfo, symbolRequirements} from "../unicodeInterface";
4 | import {toCodePoints} from "../builder/builder";
5 | import {GeneralCategory} from "../builder/unicode";
6 | import {VarSel15} from "../chars";
7 | import {cl} from "../helpers";
8 | import {KeyCap, SpriteRef} from "../config/boards";
9 | import {Fragment, h} from "preact";
10 | import {Version} from "../osversion";
11 | import {supportsRequirements} from "../utils/emojiSupportTest";
12 |
13 | function meetsRequirements(symbol: string, os: Version) {
14 | const req = symbolRequirements(symbol);
15 | return supportsRequirements(req, os);
16 | }
17 |
18 | export function Symbol({symbol}: { symbol: KeyCap }) {
19 | const os = useContext(OSContext);
20 | return useMemo(() => {
21 | if (typeof symbol === 'string') {
22 | if ([...symbol].length == 1) {
23 | const info = charInfo(toCodePoints(symbol)[0]);
24 | if (info?.ca === GeneralCategory.Space_Separator || info?.ca === GeneralCategory.Format || info?.ca === GeneralCategory.Control) {
25 | return {info.n}
26 | } else if (info?.ca === GeneralCategory.Nonspacing_Mark) {
27 | symbol = '◌' + symbol;
28 | }
29 | }
30 | // here we consider that symbol = 1 grapheme cluster
31 | // note that the browser doesn't apply the text-style selector by itself since the chars are in different fonts
32 | // also, we may need a fallback for a sequence so font-family fallback won't work either
33 | const fallbackFont = !meetsRequirements(symbol, os);
34 | const textStyle = symbol.endsWith(VarSel15);
35 | return
36 | {symbol}
37 |
38 | } else {
39 | return
40 | }
41 | }, [symbol, os]);
42 | }
43 |
44 | export function Sprite({symbol}: { symbol: SpriteRef }) {
45 | const plugins = useContext(PluginsContext);
46 | for (const plugin of plugins) {
47 | const spriteMap = plugin.data.spriteMaps?.[symbol.spriteMap];
48 | if (spriteMap && spriteMap.index[symbol.sprite]) {
49 | const scale = (spriteMap.width + 2 * (spriteMap.padding ?? 0)) / spriteMap.width;
50 | return
56 | }
57 | }
58 | return ? ;
59 | }
60 |
--------------------------------------------------------------------------------
/wwwassets/script/layout.ts:
--------------------------------------------------------------------------------
1 | import {VK} from "./layout/vk";
2 | import {ExtraSC, SC} from "./layout/sc";
3 | import {fromEntries} from "./helpers";
4 |
5 | export type SystemLayout = {
6 | readonly [id in SC]: {
7 | vk: VK,
8 | name: string
9 | }
10 | };
11 |
12 | export const SystemLayoutUS: SystemLayout = {
13 | [SC.None]: {vk: 0, name: ""},
14 | [SC.Esc]: {vk: 27, name: "Esc"},
15 | [SC.Digit1]: {vk: 49, name: "1"},
16 | [SC.Digit2]: {vk: 50, name: "2"},
17 | [SC.Digit3]: {vk: 51, name: "3"},
18 | [SC.Digit4]: {vk: 52, name: "4"},
19 | [SC.Digit5]: {vk: 53, name: "5"},
20 | [SC.Digit6]: {vk: 54, name: "6"},
21 | [SC.Digit7]: {vk: 55, name: "7"},
22 | [SC.Digit8]: {vk: 56, name: "8"},
23 | [SC.Digit9]: {vk: 57, name: "9"},
24 | [SC.Digit0]: {vk: 48, name: "0"},
25 | [SC.Minus]: {vk: 189, name: "-"},
26 | [SC.Equal]: {vk: 187, name: "="},
27 | [SC.Backspace]: {vk: 8, name: "Backspace"},
28 | [SC.Tab]: {vk: 9, name: "Tab"},
29 | [SC.Q]: {vk: 81, name: "q"},
30 | [SC.W]: {vk: 87, name: "w"},
31 | [SC.E]: {vk: 69, name: "e"},
32 | [SC.R]: {vk: 82, name: "r"},
33 | [SC.T]: {vk: 84, name: "t"},
34 | [SC.Y]: {vk: 89, name: "y"},
35 | [SC.U]: {vk: 85, name: "u"},
36 | [SC.I]: {vk: 73, name: "i"},
37 | [SC.O]: {vk: 79, name: "o"},
38 | [SC.P]: {vk: 80, name: "p"},
39 | [SC.LeftBrace]: {vk: 219, name: "["},
40 | [SC.RightBrace]: {vk: 221, name: "]"},
41 | [SC.Enter]: {vk: 13, name: "Enter"},
42 | [SC.Ctrl]: {vk: 162, name: "Ctrl"},
43 | [SC.A]: {vk: 65, name: "a"},
44 | [SC.S]: {vk: 83, name: "s"},
45 | [SC.D]: {vk: 68, name: "d"},
46 | [SC.F]: {vk: 70, name: "f"},
47 | [SC.G]: {vk: 71, name: "g"},
48 | [SC.H]: {vk: 72, name: "h"},
49 | [SC.J]: {vk: 74, name: "j"},
50 | [SC.K]: {vk: 75, name: "k"},
51 | [SC.L]: {vk: 76, name: "l"},
52 | [SC.Semicolon]: {vk: 186, name: ";"},
53 | [SC.Apostrophe]: {vk: 222, name: "'"},
54 | [SC.Backtick]: {vk: 192, name: "`"},
55 | [SC.Shift]: {vk: 160, name: "Shift"},
56 | [SC.Backslash]: {vk: 220, name: "\\"},
57 | [SC.Z]: {vk: 90, name: "z"},
58 | [SC.X]: {vk: 88, name: "x"},
59 | [SC.C]: {vk: 67, name: "c"},
60 | [SC.V]: {vk: 86, name: "v"},
61 | [SC.B]: {vk: 66, name: "b"},
62 | [SC.N]: {vk: 78, name: "n"},
63 | [SC.M]: {vk: 77, name: "m"},
64 | [SC.Comma]: {vk: 188, name: ","},
65 | [SC.Period]: {vk: 190, name: "."},
66 | [SC.Slash]: {vk: 191, name: "/"},
67 | [SC.RightShift]: {vk: 16, name: "Right Shift"},
68 | [SC.NumMult]: {vk: 106, name: "Num *"},
69 | [SC.Alt]: {vk: 164, name: "Alt"},
70 | [SC.Space]: {vk: 32, name: "Space"},
71 | [SC.CapsLock]: {vk: 20, name: "Caps Lock"},
72 | [SC.F1]: {vk: 112, name: "F1"},
73 | [SC.F2]: {vk: 113, name: "F2"},
74 | [SC.F3]: {vk: 114, name: "F3"},
75 | [SC.F4]: {vk: 115, name: "F4"},
76 | [SC.F5]: {vk: 116, name: "F5"},
77 | [SC.F6]: {vk: 117, name: "F6"},
78 | [SC.F7]: {vk: 118, name: "F7"},
79 | [SC.F8]: {vk: 119, name: "F8"},
80 | [SC.F9]: {vk: 120, name: "F9"},
81 | [SC.F10]: {vk: 121, name: "F10"},
82 | [SC.Pause]: {vk: 19, name: "Pause"},
83 | [SC.ScrollLock]: {vk: 145, name: "Scroll Lock"},
84 | [SC.Num7]: {vk: 103, name: "Num 7"},
85 | [SC.Num8]: {vk: 104, name: "Num 8"},
86 | [SC.Num9]: {vk: 105, name: "Num 9"},
87 | [SC.NumSub]: {vk: 109, name: "Num -"},
88 | [SC.Num4]: {vk: 100, name: "Num 4"},
89 | [SC.Num5]: {vk: 101, name: "Num 5"},
90 | [SC.Num6]: {vk: 102, name: "Num 6"},
91 | [SC.NumAdd]: {vk: 107, name: "Num +"},
92 | [SC.Num1]: {vk: 97, name: "Num 1"},
93 | [SC.Num2]: {vk: 98, name: "Num 2"},
94 | [SC.Num3]: {vk: 99, name: "Num 3"},
95 | [SC.Num0]: {vk: 96, name: "Num 0"},
96 | [SC.NumDecimal]: {vk: 110, name: "Num Del"},
97 | [SC.SysReq]: {vk: 44, name: "Sys Req"},
98 | [SC.LessThan]: {vk: 226, name: "\\"},
99 | [SC.F11]: {vk: 122, name: "F11"},
100 | [SC.F12]: {vk: 123, name: "F12"},
101 | [SC.RightCtrl]: {vk: 163, name: "Right Ctrl"},
102 | [SC.NumDiv]: {vk: 111, name: "Num /"},
103 | [SC.PrintScreen]: {vk: 44, name: "Prnt Scrn"},
104 | [SC.RightAlt]: {vk: 165, name: "Right Alt"},
105 | [SC.NumLock]: {vk: 144, name: "Num Lock"},
106 | [SC.Win]: {vk: 91, name: "Win"},
107 | ...fromEntries(ExtraSC.map(sc => [sc, {vk: VK.None, name: ''}] as const)),
108 | }
109 |
110 | export type KeyCodesList = readonly SC[];
111 | export const DigitsRow = [
112 | SC.Digit1,
113 | SC.Digit2,
114 | SC.Digit3,
115 | SC.Digit4,
116 | SC.Digit5,
117 | SC.Digit6,
118 | SC.Digit7,
119 | SC.Digit8,
120 | SC.Digit9,
121 | SC.Digit0,
122 | SC.Minus,
123 | SC.Equal,
124 | ] as const;
125 | export const FirstRow = [
126 | SC.Q,
127 | SC.W,
128 | SC.E,
129 | SC.R,
130 | SC.T,
131 | SC.Y,
132 | SC.U,
133 | SC.I,
134 | SC.O,
135 | SC.P,
136 | SC.LeftBrace,
137 | SC.RightBrace,
138 | ] as const;
139 | export const SecondRow = [
140 | SC.A,
141 | SC.S,
142 | SC.D,
143 | SC.F,
144 | SC.G,
145 | SC.H,
146 | SC.J,
147 | SC.K,
148 | SC.L,
149 | SC.Semicolon,
150 | SC.Apostrophe,
151 | SC.Backslash,
152 | ] as const;
153 | export const ThirdRow = [
154 | SC.Z,
155 | SC.X,
156 | SC.C,
157 | SC.V,
158 | SC.B,
159 | SC.N,
160 | SC.M,
161 | SC.Comma,
162 | SC.Period,
163 | SC.Slash,
164 | ] as const;
165 | export const SearchKeyCodesTable: KeyCodesList = [
166 | SC.Backtick, ...DigitsRow,
167 | SC.CapsLock, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
168 | SC.Shift, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024
169 | ];
170 | export type BaseLayout = {
171 | all: KeyCodesList;
172 | free: KeyCodesList;
173 | freeRows: KeyCodesList[];
174 | cssClass: string;
175 | }
176 | export type Layout = BaseLayout & {
177 | sys: SystemLayout;
178 | }
179 | export const AnsiLayout: BaseLayout = {
180 | all: [
181 | SC.Backtick, ...DigitsRow, //, 59, 60, 61
182 | SC.Tab, ...FirstRow, //, 62, 63, 64
183 | SC.CapsLock, ...SecondRow, //, 65, 66, 67
184 | SC.Shift, ...ThirdRow, SC.Extra00, SC.Extra01 //, 68, 87, 88
185 | ],
186 | free: [...DigitsRow, ...FirstRow, ...SecondRow, ...ThirdRow],
187 | freeRows: [DigitsRow, FirstRow, SecondRow, ThirdRow],
188 | cssClass: 'ansi-layout',
189 | };
190 | export const IsoLayout: BaseLayout = {
191 | all: [
192 | SC.Backtick, ...DigitsRow, //, 59, 60, 61
193 | SC.Tab, ...FirstRow, //, 62, 63, 64
194 | SC.CapsLock, ...SecondRow, //, 65, 66, 67
195 | SC.Shift, SC.LessThan, ...ThirdRow, SC.Extra00 //, 68, 87, 88
196 | ],
197 | free: [...DigitsRow, ...FirstRow, ...SecondRow, SC.LessThan, ...ThirdRow],
198 | freeRows: [DigitsRow, FirstRow, SecondRow, [SC.LessThan, ...ThirdRow]],
199 | cssClass: 'iso-layout',
200 | };
201 | export const SearchKeyCodes: number[] = [
202 | ...DigitsRow,
203 | 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
204 | 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024
205 | ];
206 |
--------------------------------------------------------------------------------
/wwwassets/script/layout/sc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Scancodes:
3 | * Key names are based on a US-like layout (note that the bottom-left key has been labeled LessThan)
4 | * ```
5 | * ` 1 2 3 4 5 6 7 8 9 0 - +
6 | * Q W E R T Y U I O P [ ] \
7 | * A S D F G H J K L ; '
8 | * < Z X C V B N M , . /
9 | * ```
10 | */
11 | export const enum SC {
12 | None = 0,
13 | Esc = 1,
14 | F1 = 59,
15 | F2 = 60,
16 | F3 = 61,
17 | F4 = 62,
18 | F5 = 63,
19 | F6 = 64,
20 | F7 = 65,
21 | F8 = 66,
22 | F9 = 67,
23 | F10 = 68,
24 | F11 = 87,
25 | F12 = 88,
26 |
27 | Backspace = 14,
28 | Tab = 15,
29 | CapsLock = 58,
30 | Enter = 28,
31 | Shift = 42,
32 | RightShift = 54,
33 | Ctrl = 29,
34 | RightCtrl = 285,
35 | Win = 91,
36 | Alt = 56,
37 | RightAlt = 312,
38 | /** Swiss layout: § */
39 | Backtick = 41,
40 | Digit1 = 2,
41 | Digit2 = 3,
42 | Digit3 = 4,
43 | Digit4 = 5,
44 | Digit5 = 6,
45 | Digit6 = 7,
46 | Digit7 = 8,
47 | Digit8 = 9,
48 | Digit9 = 10,
49 | Digit0 = 11,
50 | /** Swiss layout: ' */
51 | Minus = 12,
52 | /** Swiss layout: ^ */
53 | Equal = 13,
54 | Q = 16,
55 | W = 17,
56 | E = 18,
57 | R = 19,
58 | T = 20,
59 | /** Swiss layout: Z */
60 | Y = 21,
61 | U = 22,
62 | I = 23,
63 | O = 24,
64 | P = 25,
65 | /** Swiss layout: ÈÜ */
66 | LeftBrace = 26,
67 | /** Swiss layout: ¨ */
68 | RightBrace = 27,
69 | /** Swiss layout: $ */
70 | Backslash = 43,
71 | A = 30,
72 | S = 31,
73 | D = 32,
74 | F = 33,
75 | G = 34,
76 | H = 35,
77 | J = 36,
78 | K = 37,
79 | L = 38,
80 | /** Swiss layout: ÉÖ */
81 | Semicolon = 39,
82 | /** Swiss layout: ÀÄ */
83 | Apostrophe = 40,
84 | /** Swiss layout: <, does not exist on US layout */
85 | LessThan = 86,
86 | /** Swiss layout: Y */
87 | Z = 44,
88 | X = 45,
89 | C = 46,
90 | V = 47,
91 | B = 48,
92 | N = 49,
93 | M = 50,
94 | /** Swiss layout: , */
95 | Comma = 51,
96 | /** Swiss layout: . */
97 | Period = 52,
98 | /** Swiss layout: - */
99 | Slash = 53,
100 |
101 | Space = 57,
102 |
103 | Pause = 69,
104 | ScrollLock = 70,
105 | SysReq = 84,
106 | PrintScreen = 311,
107 |
108 | NumLock = 325,
109 | NumMult = 55,
110 | NumSub = 74,
111 | NumAdd = 78,
112 | NumDiv = 309,
113 | Num0 = 82,
114 | NumDecimal = 83,
115 | Num1 = 79,
116 | Num2 = 80,
117 | Num3 = 81,
118 | Num4 = 75,
119 | Num5 = 76,
120 | Num6 = 77,
121 | Num7 = 71,
122 | Num8 = 72,
123 | Num9 = 73,
124 |
125 | Extra00 = 1000,
126 | Extra01 = 1001,
127 | Extra02 = 1002,
128 | Extra03 = 1003,
129 | Extra04 = 1004,
130 | Extra05 = 1005,
131 | Extra06 = 1006,
132 | Extra07 = 1007,
133 | Extra08 = 1008,
134 | Extra09 = 1009,
135 | Extra10 = 1010,
136 | Extra11 = 1011,
137 | Extra12 = 1012,
138 | Extra13 = 1013,
139 | Extra14 = 1014,
140 | Extra15 = 1015,
141 | Extra16 = 1016,
142 | Extra17 = 1017,
143 | Extra18 = 1018,
144 | Extra19 = 1019,
145 | Extra20 = 1020,
146 | Extra21 = 1021,
147 | Extra22 = 1022,
148 | Extra23 = 1023,
149 | Extra24 = 1024,
150 | Extra25 = 1025,
151 | Extra26 = 1026,
152 | Extra27 = 1027,
153 | Extra28 = 1028,
154 | Extra29 = 1029,
155 | Extra30 = 1030,
156 | Extra31 = 1031,
157 | Extra32 = 1032,
158 | Extra33 = 1033,
159 | Extra34 = 1034,
160 | Extra35 = 1035,
161 | Extra36 = 1036,
162 | Extra37 = 1037,
163 | Extra38 = 1038,
164 | Extra39 = 1039,
165 | Extra40 = 1040,
166 | Extra41 = 1041,
167 | Extra42 = 1042,
168 | Extra43 = 1043,
169 | Extra44 = 1044,
170 | Extra45 = 1045,
171 | Extra46 = 1046,
172 | Extra47 = 1047,
173 | Extra48 = 1048,
174 | Extra49 = 1049,
175 | }
176 |
177 | export const ExtraSC = [
178 | SC.Extra00, SC.Extra01, SC.Extra02, SC.Extra03, SC.Extra04, SC.Extra05, SC.Extra06, SC.Extra07, SC.Extra08, SC.Extra09,
179 | SC.Extra10, SC.Extra11, SC.Extra12, SC.Extra13, SC.Extra14, SC.Extra15, SC.Extra16, SC.Extra17, SC.Extra18, SC.Extra19,
180 | SC.Extra20, SC.Extra21, SC.Extra22, SC.Extra23, SC.Extra24, SC.Extra25, SC.Extra26, SC.Extra27, SC.Extra28, SC.Extra29,
181 | SC.Extra30, SC.Extra31, SC.Extra32, SC.Extra33, SC.Extra34, SC.Extra35, SC.Extra36, SC.Extra37, SC.Extra38, SC.Extra39,
182 | SC.Extra40, SC.Extra41, SC.Extra42, SC.Extra43, SC.Extra44, SC.Extra45, SC.Extra46, SC.Extra47, SC.Extra48, SC.Extra49,
183 | ] as const;
184 |
--------------------------------------------------------------------------------
/wwwassets/script/layout/vk.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Windows Virtual Key codes
3 | * Note that e.g. wherever the letter A is placed on the keyboard, it'll have the same VK but may have a different SC
4 | * ```
5 | * ` 1 2 3 4 5 6 7 8 9 0 - +
6 | * Q W E R T Y U I O P [ ] \
7 | * A S D F G H J K L ; '
8 | * < Z X C V B N M , . /
9 | * ```
10 | */
11 | export const enum VK {
12 | None = 0,
13 |
14 | Backspace = 8,
15 | Tab = 9,
16 | Enter = 13,
17 | RightShift = 16,
18 | Pause = 19,
19 | CapsLock = 20,
20 | Esc = 27,
21 |
22 | Space = 32,
23 |
24 | SysReq = 44,
25 | PrintScreen = 44,
26 |
27 | Digit0 = 48,
28 | Digit1 = 49,
29 | Digit2 = 50,
30 | Digit3 = 51,
31 | Digit4 = 52,
32 | Digit5 = 53,
33 | Digit6 = 54,
34 | Digit7 = 55,
35 | Digit8 = 56,
36 | Digit9 = 57,
37 |
38 | A = 65,
39 | B = 66,
40 | C = 67,
41 | D = 68,
42 | E = 69,
43 | F = 70,
44 | G = 71,
45 | H = 72,
46 | I = 73,
47 | J = 74,
48 | K = 75,
49 | L = 76,
50 | M = 77,
51 | N = 78,
52 | O = 79,
53 | P = 80,
54 | Q = 81,
55 | R = 82,
56 | S = 83,
57 | T = 84,
58 | U = 85,
59 | V = 86,
60 | W = 87,
61 | X = 88,
62 | Y = 89,
63 | Z = 90,
64 |
65 | Win = 91,
66 |
67 | Num0 = 96,
68 | Num1 = 97,
69 | Num2 = 98,
70 | Num3 = 99,
71 | Num4 = 100,
72 | Num5 = 101,
73 | Num6 = 102,
74 | Num7 = 103,
75 | Num8 = 104,
76 | Num9 = 105,
77 | NumMult = 106,
78 | NumAdd = 107,
79 | NumSub = 109,
80 | NumDecimal = 110,
81 | NumDiv = 111,
82 |
83 | F1 = 112,
84 | F2 = 113,
85 | F3 = 114,
86 | F4 = 115,
87 | F5 = 116,
88 | F6 = 117,
89 | F7 = 118,
90 | F8 = 119,
91 | F9 = 120,
92 | F10 = 121,
93 | F11 = 122,
94 | F12 = 123,
95 |
96 | NumLock = 144,
97 | ScrollLock = 145,
98 |
99 | Shift = 160,
100 | Ctrl = 162,
101 | RightCtrl = 163,
102 | Alt = 164,
103 | RightAlt = 165,
104 |
105 | Semicolon = 186,
106 | Equal = 187,
107 | Comma = 188,
108 | Minus = 189,
109 | Period = 190,
110 | Slash = 191,
111 | Backtick = 192,
112 | LeftBrace = 219,
113 | Backslash = 220,
114 | RightBrace = 221,
115 | Apostrophe = 222,
116 | LessThan = 226,
117 | }
118 |
119 | export const VKMap = {
120 | a: VK.A,
121 | b: VK.B,
122 | c: VK.C,
123 | d: VK.D,
124 | e: VK.E,
125 | f: VK.F,
126 | g: VK.G,
127 | h: VK.H,
128 | i: VK.I,
129 | j: VK.J,
130 | k: VK.K,
131 | l: VK.L,
132 | m: VK.M,
133 | n: VK.N,
134 | o: VK.O,
135 | p: VK.P,
136 | q: VK.Q,
137 | r: VK.R,
138 | s: VK.S,
139 | t: VK.T,
140 | u: VK.U,
141 | v: VK.V,
142 | w: VK.W,
143 | x: VK.X,
144 | y: VK.Y,
145 | z: VK.Z,
146 |
147 | d0: VK.Digit0,
148 | d1: VK.Digit1,
149 | d2: VK.Digit2,
150 | d3: VK.Digit3,
151 | d4: VK.Digit4,
152 | d5: VK.Digit5,
153 | d6: VK.Digit6,
154 | d7: VK.Digit7,
155 | d8: VK.Digit8,
156 | d9: VK.Digit9,
157 |
158 | "0": VK.Digit0,
159 | "1": VK.Digit1,
160 | "2": VK.Digit2,
161 | "3": VK.Digit3,
162 | "4": VK.Digit4,
163 | "5": VK.Digit5,
164 | "6": VK.Digit6,
165 | "7": VK.Digit7,
166 | "8": VK.Digit8,
167 | "9": VK.Digit9,
168 |
169 | ",": VK.Comma,
170 | "-": VK.Minus,
171 | ".": VK.Period,
172 | } as const;
173 | const ReverseVkMap = new Map();
174 | for (const [k, vk] of Object.entries(VKMap)) {
175 | if (!ReverseVkMap.has(vk)) ReverseVkMap.set(vk, []);
176 | ReverseVkMap.get(vk)!.push(k as VKAbbr);
177 | }
178 |
179 | export function vkLookup(vk: VK): (VK | VKAbbr)[] {
180 | return [vk, ...(ReverseVkMap.get(vk) ?? [])];
181 | }
182 |
183 | export type VKAbbr = keyof typeof VKMap;
184 |
--------------------------------------------------------------------------------
/wwwassets/script/modules.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*?raw"
2 | {
3 | const content: string;
4 | export default content;
5 | }
6 |
--------------------------------------------------------------------------------
/wwwassets/script/osversion.ts:
--------------------------------------------------------------------------------
1 | export class Version {
2 | private readonly version: number[];
3 | private readonly known = new Set();
4 | private readonly knownLesser = new Set();
5 | private readonly knownGreater = new Set();
6 | private readonly knownSame = new Set();
7 |
8 | public constructor(version: string) {
9 | this.version = Version.parse(version);
10 | }
11 |
12 | private static parse(v: string | number): number[] {
13 | if (typeof v === "number") {
14 | return [Math.floor(v), Math.floor((v % 1) * 10)];
15 | } else {
16 | if (!/^[.0-9]+$/.test(v.trim())) {
17 | console.error("Invalid version", v);
18 | return [0];
19 | }
20 |
21 | return v.split(/\./).map(part => parseInt(part, 10));
22 | }
23 | }
24 |
25 | private static compare(a: number[], b: number[]): number {
26 | for (let i = 0; i < a.length; i++) {
27 | if (i >= b.length) return a.slice(i).some((p) => p > 0) ? 1 : 0;
28 |
29 | if (a[i] > b[i]) return 1;
30 | if (a[i] < b[i]) return -1;
31 | }
32 | if (a.length == b.length) return 0;
33 | return b.slice(a.length).some((p) => p > 0) ? -1 : 0;
34 | }
35 |
36 | private add(other: string | number) {
37 | const cmp = Version.compare(this.version, Version.parse(other));
38 | this.known.add(other);
39 |
40 | if (cmp > 0) this.knownLesser.add(other);
41 | else if (cmp < 0) this.knownGreater.add(other);
42 | else this.knownSame.add(other);
43 | }
44 |
45 | /** < operator */
46 | public lt(other: string | number): boolean {
47 | if (!this.known.has(other)) this.add(other);
48 | return this.knownGreater.has(other);
49 | }
50 |
51 | /** > operator */
52 | public gt(other: string | number): boolean {
53 | if (!this.known.has(other)) this.add(other);
54 | return this.knownLesser.has(other);
55 | }
56 |
57 | /** ≥ operator */
58 | public gte(other: string | number): boolean {
59 | return !this.lt(other);
60 | }
61 |
62 | /** ≤ operator */
63 | public lte(other: string | number): boolean {
64 | return !this.gt(other);
65 | }
66 |
67 | /** == operator */
68 | public eq(other: string | number): boolean {
69 | if (!this.known.has(other)) this.add(other);
70 | return this.knownSame.has(other);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/wwwassets/script/recentsActions.tsx:
--------------------------------------------------------------------------------
1 | import {RecentEmoji} from "./config";
2 | import {app} from "./appVar";
3 |
4 | export const FAVORITE_SCORE = 100;
5 | export const SCORE_INCR = 11;
6 | export const SCORE_DECR = 1;
7 | export const MAX_RECENT_ITEMS = 47;
8 |
9 | function sortRecent(arr: RecentEmoji[]): void {
10 | arr.sort((a, b) => b.useCount - a.useCount);
11 | }
12 |
13 | export function increaseRecent(cluster: string) {
14 | app().updateConfig(c => {
15 | const current = c.recent.find(r => r.symbol == cluster);
16 | let recent: RecentEmoji[] = [
17 | {symbol: cluster, useCount: Math.min((current?.useCount ?? 0) + SCORE_INCR, 100)},
18 | ...c.recent.filter(r => r.symbol != cluster).map(r => ({
19 | symbol: r.symbol,
20 | useCount: r.useCount < FAVORITE_SCORE ? Math.max(r.useCount - SCORE_DECR, 0) : FAVORITE_SCORE,
21 | }))
22 | ];
23 | sortRecent(recent);
24 | if (recent.length > MAX_RECENT_ITEMS) recent = recent.slice(0, MAX_RECENT_ITEMS);
25 | return {recent};
26 | }, false);
27 | }
28 |
29 | export function removeRecent(cluster: string) {
30 | app().updateConfig(c => {
31 | return {recent: c.recent.filter(r => r.symbol != cluster)}
32 | });
33 | }
34 |
35 | export function toggleFavorite(cluster: string) {
36 | app().updateConfig(c => {
37 | const r = c.recent.find(r => r.symbol == cluster);
38 | if (!r) return {recent: [{symbol: cluster, useCount: FAVORITE_SCORE}, ...c.recent]}
39 | else {
40 | const rest = c.recent.filter(r => r.symbol != cluster);
41 | if (r.useCount < FAVORITE_SCORE) {
42 | return {recent: [{symbol: cluster, useCount: FAVORITE_SCORE}, ...rest]}
43 | } else {
44 | return {
45 | recent: [{
46 | symbol: cluster,
47 | useCount: FAVORITE_SCORE / 2
48 | }, ...rest].sort((a, b) => b.useCount - a.useCount)
49 | }
50 | }
51 | }
52 | })
53 | }
54 |
--------------------------------------------------------------------------------
/wwwassets/script/recentsView.tsx:
--------------------------------------------------------------------------------
1 | import {useContext, useEffect, useMemo} from "preact/hooks";
2 | import {app, ConfigContext, LayoutContext} from "./appVar";
3 | import {SC} from "./layout/sc";
4 | import {BackKey, ConfigActionKey, ConfigLabelKey, ConfigToggleKey, ExitRecentKey, SearchKey} from "./key";
5 | import {Keys, mapKeysToSlots, SlottedKeys} from "./boards/utils";
6 | import {h} from "preact";
7 | import {Board} from "./board";
8 | import {Key} from "./keys/base";
9 | import {clusterName} from "./unicodeInterface";
10 | import {DigitsRow} from "./layout";
11 | import {FAVORITE_SCORE, removeRecent, SCORE_DECR, SCORE_INCR, toggleFavorite} from "./recentsActions";
12 |
13 | export class RecentBoard extends Board {
14 | constructor() {
15 | super({name: "Recent", symbol: "⟲"});
16 | }
17 |
18 | Contents = () => {
19 | useEffect(() => app().updateStatus(), []);
20 | const l = useContext(LayoutContext);
21 | const recentEmojis = useContext(ConfigContext).recent;
22 | const keys = useMemo(() => ({
23 | [SC.Backtick]: new ExitRecentKey(),
24 | [SC.Tab]: new SearchKey(),
25 | [SC.CapsLock]: new ExitRecentKey(),
26 | ...mapKeysToSlots(l.free, recentEmojis.map(r => new RecentClusterKey(r.symbol)))
27 | }), [l, recentEmojis]);
28 | return ;
29 | }
30 | }
31 |
32 | function useUseCount(cluster: string) {
33 | const recent = useContext(ConfigContext).recent;
34 | return useMemo(() => recent.find(r => r.symbol == cluster)?.useCount ?? 0, [cluster, recent]);
35 | }
36 |
37 | export class RecentClusterKey extends Key {
38 | constructor(private cluster: string) {
39 | const useCount = useUseCount(cluster);
40 | const name = clusterName(cluster);
41 | super({
42 | name: `${name}, score: ${useCount >= 100 ? "★" : useCount}`,
43 | symbol: cluster,
44 | keyType: "char",
45 | });
46 | }
47 |
48 | act() {
49 | app().send(this.cluster, {noRecent: true});
50 | }
51 |
52 | actAlternate() {
53 | app().setBoard(new RecentSettingsBoard(this.cluster));
54 | }
55 | }
56 |
57 | export class RecentSettingsBoard extends Board {
58 | constructor(private cluster: string) {
59 | super({
60 | name: `Settings for ${cluster}`,
61 | symbol: cluster,
62 | })
63 | }
64 |
65 | Contents = () => {
66 | useEffect(() => app().updateStatus(), []);
67 | const useCount = useUseCount(this.cluster);
68 | const keys: SlottedKeys = {
69 | [SC.Backtick]: new BackKey(),
70 | ...mapKeysToSlots(DigitsRow, [
71 | new ConfigToggleKey({
72 | name: "Favorite",
73 | symbol: "⭐",
74 | active: useCount >= FAVORITE_SCORE,
75 | action: () => toggleFavorite(this.cluster)
76 | }),
77 | new ConfigActionKey({
78 | name: "Remove", symbol: "🗑️", action: () => {
79 | removeRecent(this.cluster);
80 | app().back();
81 | }
82 | })
83 | ]),
84 | [SC.Q]: new ConfigLabelKey(`Score: ${useCount}, +${SCORE_INCR} when used, -${SCORE_DECR} when others used`),
85 | [SC.A]: new ConfigLabelKey(`Favorite when score ≥ ${FAVORITE_SCORE}`),
86 | }
87 | return
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/wwwassets/script/searchView.tsx:
--------------------------------------------------------------------------------
1 | import {h} from "preact";
2 | import {SearchKeyCodes, SearchKeyCodesTable} from "./layout";
3 | import {Board} from "./board";
4 | import {search} from "./emojis";
5 | import {useCallback, useContext, useEffect, useMemo} from "preact/hooks";
6 | import {ClusterKey, ExitSearchKey, RecentKey} from "./key";
7 | import {app, SearchContext} from "./appVar";
8 | import {SC} from "./layout/sc";
9 | import {mapKeysToSlots} from "./boards/utils";
10 | import {BlankKey} from "./keys/base";
11 |
12 | export class SearchBoard extends Board {
13 | constructor() {
14 | super({name: "Search", symbol: "🔎"});
15 | }
16 |
17 | Contents(): preact.VNode {
18 | const searchText = useContext(SearchContext);
19 | const onInput = useCallback((e: InputEvent) => app().setSearchText((e.target as HTMLInputElement).value), []);
20 | useEffect(() => (document.querySelector('input[type="search"]') as HTMLInputElement)?.focus(), []);
21 | const keys = useMemo(() => ({
22 | [SC.Backtick]: new ExitSearchKey(),
23 | [SC.Tab]: new ExitSearchKey(),
24 | [SC.CapsLock]: new RecentKey(),
25 | ...mapKeysToSlots(SearchKeyCodes, search(searchText).slice(0, SearchKeyCodes.length).map((c) => new ClusterKey(c)))
26 | }), [searchText]);
27 | app().keyHandlers = keys;
28 | return
29 |
30 | {SearchKeyCodesTable.map((code) => {
31 | const K = (keys[code] ?? BlankKey);
32 | return ;
33 | })}
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/wwwassets/script/unicodeInterface.ts:
--------------------------------------------------------------------------------
1 | import {ExtendedCharInformation, getUnicodeData} from "./builder/consolidated";
2 | import {toCodePoints} from "./builder/builder";
3 | import {IconFallback, IconRequirement} from "./config/fallback";
4 | import {UnicodeEmojiGroup} from "./unidata";
5 |
6 | export const IgnoreForName = [
7 | 8205, // Zero Width Joiner,
8 | 65039, // Variation Selector-16
9 | ]
10 | const u = getUnicodeData();
11 | export const UnicodeData = u;
12 |
13 | function charName(code: number): string {
14 | if (u.chars[code]) return u.chars[code]!.n;
15 | for (const b of u.blocks) {
16 | if (code >= b.start && code <= b.end) {
17 | return `${b.name}: U+${code.toString(16)}`;
18 | }
19 | }
20 | return `U+${code.toString(16)}`;
21 | }
22 |
23 | function clusterFullName(cluster: string): string {
24 | const c = toCodePoints(cluster);
25 | if (c.length === 1) return charName(c[0]!);
26 | return c
27 | .filter(c => !IgnoreForName.includes(c))
28 | .map(c => charName(c))
29 | .join(', ');
30 | }
31 |
32 | export function charInfo(code: number): ExtendedCharInformation | undefined {
33 | return u.chars[code];
34 | }
35 |
36 | function charAliases(code: number): string[] {
37 | const info = charInfo(code);
38 | return [...info?.falias ?? [], ...info?.alias ?? []];
39 | }
40 |
41 | export function clusterName(cluster: string): string {
42 | return u.clusters[cluster]?.name ?? clusterFullName(cluster);
43 | }
44 |
45 | export function clusterVariants(cluster: string): string[] | undefined {
46 | return u.clusters[cluster]?.variants;
47 | }
48 |
49 | export function clusterAliases(cluster: string): string[] {
50 | const cp = toCodePoints(cluster);
51 | if (cp.length === 1) return charAliases(cp[0]!);
52 | return u.clusters[cluster]?.alias ?? [];
53 | }
54 |
55 | export function emojiGroup(g: UnicodeEmojiGroup): string[] {
56 | return u.groups[g.group]?.sub[g.subGroup]?.clusters ?? [];
57 | }
58 |
59 | export function symbolRequirements(cluster: string): IconRequirement {
60 | const info = u.clusters[cluster];
61 | for (const f of IconFallback) {
62 | if (info && f.version && info.version && f.version.lte(info.version)) return f.requirement;
63 | if (f.clusters && f.clusters.has(cluster)) return f.requirement;
64 | if (f.ranges) for (const r of f.ranges) {
65 | if (cluster >= r.from && cluster <= r.to) return f.requirement;
66 | }
67 | }
68 | return {type: "windows", windows: "0"};
69 | }
70 |
--------------------------------------------------------------------------------
/wwwassets/script/unidata.d.ts:
--------------------------------------------------------------------------------
1 | // This file is generated by the builder script. Do not edit.
2 | // To rebuild, click Settings > Tools > Build in the app or run 'npm run build-unidata' in the wwwassets folder
3 | export type UnicodeEmojiGroup =
4 | {group: "Smileys & Emotion", subGroup: "face-smiling"}
5 | | {group: "Smileys & Emotion", subGroup: "face-affection"}
6 | | {group: "Smileys & Emotion", subGroup: "face-tongue"}
7 | | {group: "Smileys & Emotion", subGroup: "face-hand"}
8 | | {group: "Smileys & Emotion", subGroup: "face-neutral-skeptical"}
9 | | {group: "Smileys & Emotion", subGroup: "face-sleepy"}
10 | | {group: "Smileys & Emotion", subGroup: "face-unwell"}
11 | | {group: "Smileys & Emotion", subGroup: "face-hat"}
12 | | {group: "Smileys & Emotion", subGroup: "face-glasses"}
13 | | {group: "Smileys & Emotion", subGroup: "face-concerned"}
14 | | {group: "Smileys & Emotion", subGroup: "face-negative"}
15 | | {group: "Smileys & Emotion", subGroup: "face-costume"}
16 | | {group: "Smileys & Emotion", subGroup: "cat-face"}
17 | | {group: "Smileys & Emotion", subGroup: "monkey-face"}
18 | | {group: "Smileys & Emotion", subGroup: "heart"}
19 | | {group: "Smileys & Emotion", subGroup: "emotion"}
20 | | {group: "People & Body", subGroup: "hand-fingers-open"}
21 | | {group: "People & Body", subGroup: "hand-fingers-partial"}
22 | | {group: "People & Body", subGroup: "hand-single-finger"}
23 | | {group: "People & Body", subGroup: "hand-fingers-closed"}
24 | | {group: "People & Body", subGroup: "hands"}
25 | | {group: "People & Body", subGroup: "hand-prop"}
26 | | {group: "People & Body", subGroup: "body-parts"}
27 | | {group: "People & Body", subGroup: "person"}
28 | | {group: "People & Body", subGroup: "person-gesture"}
29 | | {group: "People & Body", subGroup: "person-role"}
30 | | {group: "People & Body", subGroup: "person-fantasy"}
31 | | {group: "People & Body", subGroup: "person-activity"}
32 | | {group: "People & Body", subGroup: "person-sport"}
33 | | {group: "People & Body", subGroup: "person-resting"}
34 | | {group: "People & Body", subGroup: "family"}
35 | | {group: "People & Body", subGroup: "person-symbol"}
36 | | {group: "Component", subGroup: "skin-tone"}
37 | | {group: "Component", subGroup: "hair-style"}
38 | | {group: "Animals & Nature", subGroup: "animal-mammal"}
39 | | {group: "Animals & Nature", subGroup: "animal-bird"}
40 | | {group: "Animals & Nature", subGroup: "animal-amphibian"}
41 | | {group: "Animals & Nature", subGroup: "animal-reptile"}
42 | | {group: "Animals & Nature", subGroup: "animal-marine"}
43 | | {group: "Animals & Nature", subGroup: "animal-bug"}
44 | | {group: "Animals & Nature", subGroup: "plant-flower"}
45 | | {group: "Animals & Nature", subGroup: "plant-other"}
46 | | {group: "Food & Drink", subGroup: "food-fruit"}
47 | | {group: "Food & Drink", subGroup: "food-vegetable"}
48 | | {group: "Food & Drink", subGroup: "food-prepared"}
49 | | {group: "Food & Drink", subGroup: "food-asian"}
50 | | {group: "Food & Drink", subGroup: "food-sweet"}
51 | | {group: "Food & Drink", subGroup: "drink"}
52 | | {group: "Food & Drink", subGroup: "dishware"}
53 | | {group: "Travel & Places", subGroup: "place-map"}
54 | | {group: "Travel & Places", subGroup: "place-geographic"}
55 | | {group: "Travel & Places", subGroup: "place-building"}
56 | | {group: "Travel & Places", subGroup: "place-religious"}
57 | | {group: "Travel & Places", subGroup: "place-other"}
58 | | {group: "Travel & Places", subGroup: "transport-ground"}
59 | | {group: "Travel & Places", subGroup: "transport-water"}
60 | | {group: "Travel & Places", subGroup: "transport-air"}
61 | | {group: "Travel & Places", subGroup: "hotel"}
62 | | {group: "Travel & Places", subGroup: "time"}
63 | | {group: "Travel & Places", subGroup: "sky & weather"}
64 | | {group: "Activities", subGroup: "event"}
65 | | {group: "Activities", subGroup: "award-medal"}
66 | | {group: "Activities", subGroup: "sport"}
67 | | {group: "Activities", subGroup: "game"}
68 | | {group: "Activities", subGroup: "arts & crafts"}
69 | | {group: "Objects", subGroup: "clothing"}
70 | | {group: "Objects", subGroup: "sound"}
71 | | {group: "Objects", subGroup: "music"}
72 | | {group: "Objects", subGroup: "musical-instrument"}
73 | | {group: "Objects", subGroup: "phone"}
74 | | {group: "Objects", subGroup: "computer"}
75 | | {group: "Objects", subGroup: "light & video"}
76 | | {group: "Objects", subGroup: "book-paper"}
77 | | {group: "Objects", subGroup: "money"}
78 | | {group: "Objects", subGroup: "mail"}
79 | | {group: "Objects", subGroup: "writing"}
80 | | {group: "Objects", subGroup: "office"}
81 | | {group: "Objects", subGroup: "lock"}
82 | | {group: "Objects", subGroup: "tool"}
83 | | {group: "Objects", subGroup: "science"}
84 | | {group: "Objects", subGroup: "medical"}
85 | | {group: "Objects", subGroup: "household"}
86 | | {group: "Objects", subGroup: "other-object"}
87 | | {group: "Symbols", subGroup: "transport-sign"}
88 | | {group: "Symbols", subGroup: "warning"}
89 | | {group: "Symbols", subGroup: "arrow"}
90 | | {group: "Symbols", subGroup: "religion"}
91 | | {group: "Symbols", subGroup: "zodiac"}
92 | | {group: "Symbols", subGroup: "av-symbol"}
93 | | {group: "Symbols", subGroup: "gender"}
94 | | {group: "Symbols", subGroup: "math"}
95 | | {group: "Symbols", subGroup: "punctuation"}
96 | | {group: "Symbols", subGroup: "currency"}
97 | | {group: "Symbols", subGroup: "other-symbol"}
98 | | {group: "Symbols", subGroup: "keycap"}
99 | | {group: "Symbols", subGroup: "alphanum"}
100 | | {group: "Symbols", subGroup: "geometric"}
101 | | {group: "Flags", subGroup: "flag"}
102 | | {group: "Flags", subGroup: "country-flag"}
103 | | {group: "Flags", subGroup: "subdivision-flag"};
104 |
--------------------------------------------------------------------------------
/wwwassets/script/utils/compare.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Compare two strings in a natural way, where numbers are compared as numbers.
3 | *
4 | * @example
5 | * // returns ['1', '2', '10']
6 | * ['1', '10', '2'].sort(naturalCompare)
7 | */
8 | export function naturalCompare(a: string|null|undefined, b: string|null|undefined): number {
9 | // any null, empty or undefined comes first (!!'' == false)
10 | if (!a || !b) return +!!a - +!!b;
11 | const aParts = a.match(/[0-9]+|[^0-9]+/g)!;
12 | const bParts = b.match(/[0-9]+|[^0-9]+/g)!;
13 | for (const i of aParts.keys()) {
14 | if (bParts.length <= i) return 1;
15 | const aText = !/^[0-9]/.test(aParts[i]);
16 | const bText = !/^[0-9]/.test(bParts[i]);
17 | if (aText !== bText) return +aText - +bText;
18 | if (aText) {
19 | const cmp = aParts[i].localeCompare(bParts[i]);
20 | if (cmp !== 0) return cmp;
21 | } else {
22 | const cmp = parseInt(aParts[i], 10) - parseInt(bParts[i], 10);
23 | if (cmp !== 0) return cmp;
24 | }
25 | }
26 | return -1;
27 | }
--------------------------------------------------------------------------------
/wwwassets/script/utils/emojiSupportTest.ts:
--------------------------------------------------------------------------------
1 | import {ZeroWidthJoiner} from "../chars";
2 | import {IconRequirement} from "../config/fallback";
3 | import {unreachable} from "../helpers";
4 | import {Version} from "../osversion";
5 |
6 | let hiddenBox: HTMLDivElement | null = null;
7 | if (typeof window !== "undefined") {
8 | hiddenBox = document.createElement('div');
9 | document.body.appendChild(hiddenBox);
10 | hiddenBox.style.position = 'absolute';
11 | hiddenBox.style.left = '-1000px';
12 | hiddenBox.style.top = '-1000px';
13 | document.fonts.load("16px AdobeBlank");
14 | }
15 |
16 | const knownZWJ = new Map();
17 | const knownSymbol = new Map();
18 |
19 |
20 | export function supportsRequirements(req: IconRequirement, os: Version): boolean {
21 | if (req.type === "windows") return os.gte(req.windows);
22 | else if (req.type === "zwjEmoji") return supportsZWJ(req.zwjEmoji);
23 | else if (req.type === "emoji") return supportsSymbol(req.emoji);
24 | else return unreachable(req);
25 | }
26 |
27 | /** Check if the ZWJ sequence is supported by the base font, i.e. it's shorter than character taken separately */
28 | function supportsZWJ(sequence: string): boolean {
29 | if (!knownZWJ.has(sequence)) {
30 | const el = document.createElement('span');
31 | hiddenBox!.appendChild(el);
32 | el.textContent = sequence;
33 | const len1 = el.offsetWidth;
34 | el.textContent = sequence.replace(ZeroWidthJoiner, '');
35 | const len2 = el.offsetWidth;
36 | hiddenBox!.removeChild(el);
37 | const supported = len1 > 0 && len1 < len2;
38 | knownZWJ.set(sequence, supported);
39 | }
40 | return knownZWJ.get(sequence)!;
41 | }
42 |
43 | /** Check if the symbol is supported by the base font, i.e. a glyph is displayed using the base font when setting Adobe Blank as fallback */
44 | function supportsSymbol(symbol: string): boolean {
45 | if (!knownSymbol.has(symbol)) {
46 | const el = document.createElement('span');
47 | el.style.fontFamily = '"Segoe UI Emoji", "Segoe UI", AdobeBlank';
48 | hiddenBox!.appendChild(el);
49 | el.textContent = 'a' + symbol;
50 | const len1 = el.offsetWidth;
51 | el.textContent = 'a';
52 | const len2 = el.offsetWidth;
53 | hiddenBox!.removeChild(el);
54 | const supported = len1 > 0 && len1 > len2;
55 | knownSymbol.set(symbol, supported);
56 | }
57 | return knownSymbol.get(symbol)!;
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/wwwassets/style/legacy.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --c-background: #FFF;
3 | --c-key: #f8f8f8;
4 | --c-text: #000;
5 | --c-empty: #f8f8f8;
6 | --c-empty-text: #888;
7 | --c-label: #f8f8f8;
8 | --c-primary: #FFC83D;
9 | --border-key: solid .1vw #888;
10 | --c-shadow: rgba(255, 255, 255, .5);
11 |
12 | --dark-c-shadow: #000;
13 | }
14 |
15 | @media (prefers-color-scheme: dark) {
16 | .system-color-scheme {
17 | --c-background: #1a1a1a;
18 | --c-key: #333;
19 | --c-text: #fff;
20 | --c-empty: #252525;
21 | --c-empty-text: #fff;
22 | --c-label: #252525;
23 | --c-primary: #FFC83D;
24 | --border-key: none;
25 |
26 | --c-shadow: var(--dark-c-shadow);
27 | }
28 | }
29 |
30 | .dark-color-scheme {
31 | --c-background: #1a1a1a;
32 | --c-key: #333;
33 | --c-text: #fff;
34 | --c-empty: #252525;
35 | --c-empty-text: #fff;
36 | --c-label: #252525;
37 | --c-primary: #FFC83D;
38 | --border-key: none;
39 |
40 | --c-shadow: var(--dark-c-shadow);
41 | }
42 |
43 | .keyboard {
44 | background-color: var(--c-background);
45 | }
46 |
47 | .key{
48 | background-color: var(--c-key);
49 | border: var(--border-key);
50 | color: var(--c-text);
51 | }
52 |
53 | .key .symbol {
54 | color: var(--c-text);
55 | }
56 |
57 | .key.empty{
58 | background-color: var(--c-empty);
59 | color: var(--c-empty-text);
60 | }
61 | .key.label{
62 | background-color: var(--c-empty);
63 | color: var(--c-text);
64 | }
65 |
66 | .key.alt{
67 | box-shadow: 0 .5vw var(--c-primary) inset;
68 | }
69 |
70 | .key.active{
71 | box-shadow: 0 .5vw var(--c-text) inset;
72 | }
73 |
74 | .key.char .symbol img{
75 | box-shadow: 0 0 .2em var(--c-text);
76 | background: var(--c-background);
77 | }
78 |
79 | .searchpane input{
80 | border: solid .1vw #888;
81 | }
82 |
--------------------------------------------------------------------------------
/wwwassets/style/material.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --c-background: #EEE;
3 | --c-primary: #FFC83D;
4 | --c-key: #FFF;
5 | --c-text: #000;
6 | --c-text2: #888;
7 | --c-text-active: #000;
8 | --c-empty: #BBB;
9 | --c-shadow: rgba(255, 255, 255, .5);
10 |
11 | --dark-c-background: #222;
12 | --dark-c-key: #333;
13 | --dark-c-text: #EEE;
14 | --dark-c-text2: #888;
15 | --dark-c-text-active: #000;
16 | --dark-c-empty: #888;
17 | --dark-c-shadow: #000;
18 | }
19 |
20 | @media (prefers-color-scheme: dark) {
21 | .system-color-scheme {
22 | --c-background: var(--dark-c-background);
23 | --c-key: var(--dark-c-key);
24 | --c-text: var(--dark-c-text);
25 | --c-text2: var(--dark-c-text2);
26 | --c-text-active: var(--dark-c-text-active);
27 | --c-empty: var(--dark-c-empty);
28 | --c-shadow: var(--dark-c-shadow);
29 | }
30 | }
31 |
32 | .dark-color-scheme {
33 | --c-background: var(--dark-c-background);
34 | --c-key: var(--dark-c-key);
35 | --c-text: var(--dark-c-text);
36 | --c-text2: var(--dark-c-text2);
37 | --c-text-active: var(--dark-c-text-active);
38 | --c-empty: var(--dark-c-empty);
39 | --c-shadow: var(--dark-c-shadow);
40 | }
41 |
42 | .keyboard {
43 | background-color: var(--c-background);
44 | }
45 |
46 | .key {
47 | background-color: var(--c-key);
48 | border-radius: .5vw;
49 | color: var(--c-text);
50 | }
51 |
52 | .row {
53 | /*! padding: .2vw 0; */
54 | }
55 |
56 | .key .symbol {
57 | color: var(--c-text);
58 | }
59 |
60 | .key.empty {
61 | background-color: inherit;
62 | color: var(--c-empty);
63 | }
64 |
65 | .key.alt {
66 | background: linear-gradient(to top, var(--c-primary) .5vw, var(--c-key) .5vw);
67 | }
68 |
69 | .key.active {
70 | background: var(--c-primary);
71 | color: var(--c-text-active);
72 | }
73 |
74 | .key .keyname, .key .name {
75 | color: var(--c-text2);
76 | }
77 |
78 | .key.active .keyname, .key.active .name {
79 | color: var(--c-text-active);
80 | }
81 |
82 | .key:not(.empty):not(.label):hover {
83 | color: var(--c-text);
84 | box-shadow: 0 .2vw .4vw #444;
85 | }
86 |
87 | .searchpane input {
88 | border: solid .1vw #888;
89 | }
90 |
--------------------------------------------------------------------------------
/wwwassets/style/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --s-gap: 4px;
3 | --c-shadow: #000;
4 | --f-fallback: 'Cambria Math', Tahoma, Geneva, Verdana, sans-serif;
5 | }
6 |
7 | body, html {
8 | margin: 0;
9 | padding: 0;
10 | font-family: 'Segoe UI', 'Segoe UI Emoji', var(--f-fallback);
11 | font-size: 1.8vw;
12 | }
13 |
14 | .fallbackFont {
15 | font-family: NotoEmoji, 'Segoe UI', 'Segoe UI Emoji', var(--f-fallback);
16 | }
17 |
18 | .textStyle {
19 | font-family: 'Segoe UI', 'Segoe UI Symbol', var(--f-fallback);
20 | }
21 |
22 | * {
23 | user-select: none;
24 | -ms-user-select: none;
25 | }
26 |
27 | input {
28 | font-family: 'Segoe UI', 'Segoe UI Emoji', 'Cambria Math', Tahoma, Geneva, Verdana, sans-serif;
29 | }
30 |
31 | @font-face {
32 | font-family: NotoEmoji;
33 | src: url("../fonts/NotoColorEmoji-Regular.ttf");
34 | }
35 |
36 | @font-face {
37 | /*
38 | * Adobe Blank is a special font that maps all unicode codepoints to non-spacing non-marking glyphs.
39 | * The can be used to determine whether a character is supported by the previous fonts in a font stack.
40 | * See https://github.com/adobe-fonts/adobe-blank.
41 | */
42 | font-family: AdobeBlank;
43 | src: url("../fonts/AdobeBlank.otf");
44 | }
45 |
46 | main{
47 | display: flex;
48 | flex-direction: column;
49 | cursor: default;
50 | }
51 |
52 | main > section{
53 | flex: 1;
54 | overflow: auto;
55 | }
56 |
57 | .keyboard{
58 | display: grid;
59 | box-sizing: border-box;
60 | grid-template-columns: repeat(13, 1fr);
61 | grid-template-rows: repeat(4, 1fr);
62 | position: absolute;
63 | top: 0;
64 | left: 0;
65 | height: 100vh;
66 | width: 100vw;
67 | grid-gap: var(--s-gap);
68 | padding: var(--s-gap);
69 | }
70 |
71 | .keyboard input[type="search"] {
72 | grid-column: 1 / 14;
73 | font-size: 4vw;
74 | }
75 |
76 | .key{
77 | display: flex;
78 | list-style: none;
79 | padding: .3vw;
80 | overflow: hidden;
81 | position: relative;
82 | align-items: center;
83 | transition: background ease .05s;
84 | }
85 |
86 | .row{
87 | display: inline-flex;
88 | }
89 |
90 | .key .symbol{
91 | font-size: 3.6vw;
92 | text-align: center;
93 | flex: 1;
94 | transition: ease .1s;
95 | white-space: pre;
96 | text-shadow: var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em;
97 | }
98 | .key.char .symbol.s-space{
99 | font-size: 1.2vw;
100 | font-variant: small-caps;
101 | font-weight: bold;
102 | max-width: 100%;
103 | transform: none;
104 | hyphens: auto;
105 | word-wrap: break-word;
106 | white-space: normal;
107 | }
108 | .key .symbol img{
109 | height: 3.6vw;
110 | }
111 |
112 | .key .keyname{
113 | position: absolute;
114 | top: .2vw;
115 | left: .2vw;
116 | font-variant: small-caps;
117 | line-height: 1em;
118 | transition: ease .05s;
119 | }
120 |
121 | .key .name{
122 | font-size: 1.4vw;
123 | line-height: 1.2vw;
124 | text-align: right;
125 | position: absolute;
126 | bottom: .2vw;
127 | right: .2vw;
128 | left: .2vw;
129 | font-variant: small-caps;
130 | transition: ease .05s;
131 | }
132 |
133 | .key.char .name{
134 | display: none;
135 | }
136 |
137 | .key.char .symbol{
138 | transform: scale(1.2);
139 | }
140 |
141 | .key:hover .name{
142 | opacity: .8;
143 | }
144 |
145 | .key:not(.label):hover .symbol{
146 | transform: translateY(-4px) scale(1.8) rotate(0deg);
147 | }
148 |
149 | .key:not(.label):active .symbol{
150 | transform: translateY(-2px) scale(1.4) rotate(7deg);
151 | }
152 |
153 | .key.char .keypress {
154 | transform: scale(1) rotate(7deg);
155 | }
156 |
157 | .key:not(.lu) .uname{
158 | display: none;
159 | }
160 |
161 | .key .uname{
162 | position: absolute;
163 | bottom: .2vw;
164 | right: .2vw;
165 | line-height: 1em;
166 | transition: ease .05s;
167 | }
168 |
169 | .key.label {
170 | background-color: inherit;
171 | overflow: visible;
172 | min-width: 0;
173 | z-index: 10;
174 | }
175 |
176 | .searchpane{
177 | padding: .2vh 2.05vw;
178 | height: 25vh;
179 | box-sizing: border-box;
180 | }
181 |
182 | .searchpane input {
183 | display: block;
184 | height: 100%;
185 | border: none;
186 | width: 100%;
187 | box-sizing: border-box;
188 | padding: 1vw;
189 | }
190 |
191 | div.sprite {
192 | margin: 0 auto;
193 | height: 3.6vw;
194 | width: 3.6vw;
195 | position: relative;
196 | top: .3vw;
197 | }
198 |
--------------------------------------------------------------------------------
/wwwassets/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "noImplicitAny": true,
5 | "strict": true,
6 | "removeComments": true,
7 | "preserveConstEnums": true,
8 | "module": "AMD",
9 | "baseUrl": "lib",
10 | "paths": {
11 | "preact": [
12 | "preact/index"
13 | ]
14 | },
15 | "target": "es2022",
16 | "jsx": "react",
17 | "jsxFactory": "h",
18 | "lib": [
19 | "dom",
20 | "esnext"
21 | ],
22 | "typeRoots": [
23 | "lib"
24 | ],
25 | "moduleResolution": "node"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/wwwassets/vite.config.js:
--------------------------------------------------------------------------------
1 | import {dirname, resolve} from 'node:path'
2 | import {fileURLToPath} from 'node:url'
3 | import {defineConfig} from 'vite'
4 |
5 | const __dirname = dirname(fileURLToPath(import.meta.url))
6 |
7 | export default defineConfig({
8 | build: {
9 | lib: {
10 | entry: resolve(__dirname, 'script/entryPoint.ts'),
11 | name: 'Emoji-Keyboard',
12 | // the proper extensions will be added
13 | fileName: 'emoji-keyboard',
14 | formats: ['es'],
15 | },
16 | rollupOptions: {
17 | // make sure to externalize deps that shouldn't be bundled
18 | // into your library
19 | external: ['vue'],
20 | output: {
21 | // Provide global variables to use in the UMD build
22 | // for externalized deps
23 | globals: {
24 | vue: 'Vue',
25 | },
26 | },
27 | },
28 | },
29 | })
30 |
--------------------------------------------------------------------------------