feat: Unified icon picker (#7038)
This commit is contained in:
@@ -207,8 +207,6 @@
|
||||
"Title": "Title",
|
||||
"Published": "Published",
|
||||
"Include nested documents": "Include nested documents",
|
||||
"Emoji Picker": "Emoji Picker",
|
||||
"Remove": "Remove",
|
||||
"Module failed to load": "Module failed to load",
|
||||
"Loading Failed": "Loading Failed",
|
||||
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.",
|
||||
@@ -244,11 +242,27 @@
|
||||
"Group members": "Group members",
|
||||
"{{authorName}} created <3></3>": "{{authorName}} created <3></3>",
|
||||
"{{authorName}} opened <3></3>": "{{authorName}} opened <3></3>",
|
||||
"Search emoji": "Search emoji",
|
||||
"Search icons": "Search icons",
|
||||
"Choose default skin tone": "Choose default skin tone",
|
||||
"Show menu": "Show menu",
|
||||
"Choose an icon": "Choose an icon",
|
||||
"Filter": "Filter",
|
||||
"Loading": "Loading",
|
||||
"Icon Picker": "Icon Picker",
|
||||
"Icons": "Icons",
|
||||
"Emojis": "Emojis",
|
||||
"Remove": "Remove",
|
||||
"All": "All",
|
||||
"Frequently Used": "Frequently Used",
|
||||
"Search Results": "Search Results",
|
||||
"Smileys & People": "Smileys & People",
|
||||
"Animals & Nature": "Animals & Nature",
|
||||
"Food & Drink": "Food & Drink",
|
||||
"Activity": "Activity",
|
||||
"Travel & Places": "Travel & Places",
|
||||
"Objects": "Objects",
|
||||
"Symbols": "Symbols",
|
||||
"Flags": "Flags",
|
||||
"Select a color": "Select a color",
|
||||
"Loading": "Loading",
|
||||
"Search": "Search",
|
||||
"Permission": "Permission",
|
||||
"View only": "View only",
|
||||
@@ -765,7 +779,6 @@
|
||||
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
|
||||
"Search titles only": "Search titles only",
|
||||
"No documents found for your search filters.": "No documents found for your search filters.",
|
||||
"Search Results": "Search Results",
|
||||
"API key copied to clipboard": "API key copied to clipboard",
|
||||
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
|
||||
"Personal keys": "Personal keys",
|
||||
@@ -858,7 +871,6 @@
|
||||
"New group": "New group",
|
||||
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
|
||||
"No groups have been created yet": "No groups have been created yet",
|
||||
"All": "All",
|
||||
"Create a group": "Create a group",
|
||||
"Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.": "Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.",
|
||||
"Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)": "Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)",
|
||||
@@ -869,6 +881,7 @@
|
||||
"Enterprise": "Enterprise",
|
||||
"Recent imports": "Recent imports",
|
||||
"Everyone that has signed into {{appName}} is listed here. It’s possible that there are other users who have access through {{signinMethods}} but haven’t signed in yet.": "Everyone that has signed into {{appName}} is listed here. It’s possible that there are other users who have access through {{signinMethods}} but haven’t signed in yet.",
|
||||
"Filter": "Filter",
|
||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||
"Document updated": "Document updated",
|
||||
"Receive a notification when a document you are subscribed to is edited": "Receive a notification when a document you are subscribed to is edited",
|
||||
|
||||
@@ -61,6 +61,8 @@ const buildBaseTheme = (input: Partial<Colors>) => {
|
||||
"-apple-system, BlinkMacSystemFont, Inter, 'Segoe UI', Roboto, Oxygen, sans-serif",
|
||||
fontFamilyMono:
|
||||
"'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||
fontFamilyEmoji:
|
||||
"Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Segoe UI, Twemoji Mozilla, Noto Color Emoji, Android Emoji",
|
||||
fontWeightRegular: 400,
|
||||
fontWeightMedium: 500,
|
||||
fontWeightBold: 600,
|
||||
|
||||
@@ -230,6 +230,8 @@ export type NavigationNode = {
|
||||
title: string;
|
||||
url: string;
|
||||
emoji?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
children: NavigationNode[];
|
||||
isDraft?: boolean;
|
||||
collectionId?: string;
|
||||
@@ -405,3 +407,43 @@ export type ProsemirrorDoc = {
|
||||
type: "doc";
|
||||
content: ProsemirrorData[];
|
||||
};
|
||||
|
||||
export enum IconType {
|
||||
Outline = "outline",
|
||||
Emoji = "emoji",
|
||||
}
|
||||
|
||||
export enum EmojiCategory {
|
||||
People = "People",
|
||||
Nature = "Nature",
|
||||
Foods = "Foods",
|
||||
Activity = "Activity",
|
||||
Places = "Places",
|
||||
Objects = "Objects",
|
||||
Symbols = "Symbols",
|
||||
Flags = "Flags",
|
||||
}
|
||||
|
||||
export enum EmojiSkinTone {
|
||||
Default = "Default",
|
||||
Light = "Light",
|
||||
MediumLight = "MediumLight",
|
||||
Medium = "Medium",
|
||||
MediumDark = "MediumDark",
|
||||
Dark = "Dark",
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type EmojiVariants = {
|
||||
[EmojiSkinTone.Default]: Emoji;
|
||||
[EmojiSkinTone.Light]?: Emoji;
|
||||
[EmojiSkinTone.MediumLight]?: Emoji;
|
||||
[EmojiSkinTone.Medium]?: Emoji;
|
||||
[EmojiSkinTone.MediumDark]?: Emoji;
|
||||
[EmojiSkinTone.Dark]?: Emoji;
|
||||
};
|
||||
|
||||
@@ -210,7 +210,7 @@ export class IconLibrary {
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(Boolean);
|
||||
.filter((icon: string | undefined): icon is string => !!icon);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ export const sortNavigationNodes = (
|
||||
|
||||
export const colorPalette = [
|
||||
"#4E5C6E",
|
||||
"#0366d6",
|
||||
"#0366D6",
|
||||
"#9E5CF7",
|
||||
"#FF825C",
|
||||
"#FF5C80",
|
||||
|
||||
136
shared/utils/emoji.ts
Normal file
136
shared/utils/emoji.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import RawData from "@emoji-mart/data";
|
||||
import type { EmojiMartData, Skin } from "@emoji-mart/data";
|
||||
import { init, Data } from "emoji-mart";
|
||||
import FuzzySearch from "fuzzy-search";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { Emoji, EmojiCategory, EmojiSkinTone, EmojiVariants } from "../types";
|
||||
import { isMac } from "./browser";
|
||||
|
||||
const isMacEnv = isMac();
|
||||
|
||||
init({ data: RawData });
|
||||
|
||||
// Data has the pre-processed "search" terms.
|
||||
const TypedData = Data as EmojiMartData;
|
||||
|
||||
const flagEmojiIds =
|
||||
TypedData.categories
|
||||
.filter(({ id }) => id === EmojiCategory.Flags.toLowerCase())
|
||||
.map(({ emojis }) => emojis)[0] ?? [];
|
||||
|
||||
const Categories = TypedData.categories.filter(
|
||||
({ id }) => isMacEnv || capitalize(id) !== EmojiCategory.Flags
|
||||
);
|
||||
|
||||
const Emojis = Object.fromEntries(
|
||||
Object.entries(TypedData.emojis).filter(
|
||||
([id]) => isMacEnv || !flagEmojiIds.includes(id)
|
||||
)
|
||||
);
|
||||
|
||||
const searcher = new FuzzySearch(Object.values(Emojis), ["search"], {
|
||||
caseSensitive: false,
|
||||
sort: true,
|
||||
});
|
||||
|
||||
// Codes defined by unicode.org
|
||||
const SKINTONE_CODE_TO_ENUM = {
|
||||
"1f3fb": EmojiSkinTone.Light,
|
||||
"1f3fc": EmojiSkinTone.MediumLight,
|
||||
"1f3fd": EmojiSkinTone.Medium,
|
||||
"1f3fe": EmojiSkinTone.MediumDark,
|
||||
"1f3ff": EmojiSkinTone.Dark,
|
||||
};
|
||||
|
||||
type GetVariantsProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
skins: Skin[];
|
||||
};
|
||||
|
||||
const getVariants = ({ id, name, skins }: GetVariantsProps): EmojiVariants =>
|
||||
skins.reduce((obj, skin) => {
|
||||
const skinToneCode = skin.unified.split("-")[1];
|
||||
const skinToneType =
|
||||
SKINTONE_CODE_TO_ENUM[skinToneCode] ?? EmojiSkinTone.Default;
|
||||
obj[skinToneType] = { id, name, value: skin.native } satisfies Emoji;
|
||||
return obj;
|
||||
}, {} as EmojiVariants);
|
||||
|
||||
const EMOJI_ID_TO_VARIANTS = Object.entries(Emojis).reduce(
|
||||
(obj, [id, emoji]) => {
|
||||
obj[id] = getVariants({
|
||||
id,
|
||||
name: emoji.name,
|
||||
skins: emoji.skins,
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, EmojiVariants>
|
||||
);
|
||||
|
||||
const CATEGORY_TO_EMOJI_IDS: Record<EmojiCategory, string[]> =
|
||||
Categories.reduce((obj, { id, emojis }) => {
|
||||
const category = EmojiCategory[capitalize(id)];
|
||||
if (!category) {
|
||||
return obj;
|
||||
}
|
||||
obj[category] = emojis;
|
||||
return obj;
|
||||
}, {} as Record<EmojiCategory, string[]>);
|
||||
|
||||
export const getEmojis = ({
|
||||
ids,
|
||||
skinTone,
|
||||
}: {
|
||||
ids: string[];
|
||||
skinTone: EmojiSkinTone;
|
||||
}): Emoji[] =>
|
||||
ids.map(
|
||||
(id) =>
|
||||
EMOJI_ID_TO_VARIANTS[id][skinTone] ??
|
||||
EMOJI_ID_TO_VARIANTS[id][EmojiSkinTone.Default]
|
||||
);
|
||||
|
||||
export const getEmojisWithCategory = ({
|
||||
skinTone,
|
||||
}: {
|
||||
skinTone: EmojiSkinTone;
|
||||
}): Record<EmojiCategory, Emoji[]> =>
|
||||
Object.keys(CATEGORY_TO_EMOJI_IDS).reduce((obj, category: EmojiCategory) => {
|
||||
const emojiIds = CATEGORY_TO_EMOJI_IDS[category];
|
||||
const emojis = emojiIds.map(
|
||||
(emojiId) =>
|
||||
EMOJI_ID_TO_VARIANTS[emojiId][skinTone] ??
|
||||
EMOJI_ID_TO_VARIANTS[emojiId][EmojiSkinTone.Default]
|
||||
);
|
||||
obj[category] = emojis;
|
||||
return obj;
|
||||
}, {} as Record<EmojiCategory, Emoji[]>);
|
||||
|
||||
export const getEmojiVariants = ({ id }: { id: string }) =>
|
||||
EMOJI_ID_TO_VARIANTS[id];
|
||||
|
||||
export const search = ({
|
||||
query,
|
||||
skinTone,
|
||||
}: {
|
||||
query: string;
|
||||
skinTone?: EmojiSkinTone;
|
||||
}) => {
|
||||
const queryLowercase = query.toLowerCase();
|
||||
const emojiSkinTone = skinTone ?? EmojiSkinTone.Default;
|
||||
|
||||
const matchedEmojis = searcher
|
||||
.search(queryLowercase)
|
||||
.map(
|
||||
(emoji) =>
|
||||
EMOJI_ID_TO_VARIANTS[emoji.id][emojiSkinTone] ??
|
||||
EMOJI_ID_TO_VARIANTS[emoji.id][EmojiSkinTone.Default]
|
||||
);
|
||||
return sortBy(matchedEmojis, (emoji) => {
|
||||
const nlc = emoji.name.toLowerCase();
|
||||
return query === nlc ? -1 : nlc.startsWith(queryLowercase) ? 0 : 1;
|
||||
});
|
||||
};
|
||||
13
shared/utils/icon.ts
Normal file
13
shared/utils/icon.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IconType } from "../types";
|
||||
import { IconLibrary } from "./IconLibrary";
|
||||
|
||||
const outlineIconNames = new Set(Object.keys(IconLibrary.mapping));
|
||||
|
||||
export const determineIconType = (
|
||||
icon?: string | null
|
||||
): IconType | undefined => {
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
return outlineIconNames.has(icon) ? IconType.Outline : IconType.Emoji;
|
||||
};
|
||||
Reference in New Issue
Block a user