feat: Unified icon picker (#7038)

This commit is contained in:
Hemachandar
2024-06-23 19:01:18 +05:30
committed by GitHub
parent 56d90e6bc3
commit 6fd3a0fa8a
83 changed files with 2302 additions and 852 deletions

View File

@@ -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 youre looking for.": "We were unable to find the page youre 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. Its possible that there are other users who have access through {{signinMethods}} but havent signed in yet.": "Everyone that has signed into {{appName}} is listed here. Its possible that there are other users who have access through {{signinMethods}} but havent 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",

View File

@@ -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,

View File

@@ -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;
};

View File

@@ -210,7 +210,7 @@ export class IconLibrary {
}
return undefined;
})
.filter(Boolean);
.filter((icon: string | undefined): icon is string => !!icon);
}
/**

View File

@@ -31,7 +31,7 @@ export const sortNavigationNodes = (
export const colorPalette = [
"#4E5C6E",
"#0366d6",
"#0366D6",
"#9E5CF7",
"#FF825C",
"#FF5C80",

136
shared/utils/emoji.ts Normal file
View 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
View 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;
};