Add 80+ additional icons from FontAwesome (#6803)

* Add 80+ additional icons from FontAwesome

* fix: color switch transition, add 6 more icons to fill out grid

* Add strict validation for collection icon

* fix: Avoid import from app in server
This commit is contained in:
Tom Moor
2024-04-13 12:33:07 -06:00
committed by GitHub
parent 689886797c
commit 765ae7b298
18 changed files with 429 additions and 155 deletions

View File

@@ -5,13 +5,13 @@ import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import { randomElement } from "@shared/random"; import { randomElement } from "@shared/random";
import { CollectionPermission } from "@shared/types"; import { CollectionPermission } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections"; import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations"; import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import IconPicker from "~/components/IconPicker"; import IconPicker from "~/components/IconPicker";
import { IconLibrary } from "~/components/Icons/IconLibrary";
import Input from "~/components/Input"; import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission"; import InputSelectPermission from "~/components/InputSelectPermission";
import Switch from "~/components/Switch"; import Switch from "~/components/Switch";

View File

@@ -7,6 +7,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { s, ellipsis } from "@shared/styles"; import { s, ellipsis } from "@shared/styles";
import Document from "~/models/Document"; import Document from "~/models/Document";
import Pin from "~/models/Pin"; import Pin from "~/models/Pin";
@@ -17,7 +18,6 @@ import useStores from "~/hooks/useStores";
import { hover } from "~/styles"; import { hover } from "~/styles";
import CollectionIcon from "./Icons/CollectionIcon"; import CollectionIcon from "./Icons/CollectionIcon";
import EmojiIcon from "./Icons/EmojiIcon"; import EmojiIcon from "./Icons/EmojiIcon";
import Squircle from "./Squircle";
import Text from "./Text"; import Text from "./Text";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";

View File

@@ -1,38 +1,3 @@
import { CSSProperties } from "react"; import Flex from "@shared/components/Flex";
import styled from "styled-components";
type JustifyValues = CSSProperties["justifyContent"];
type AlignValues = CSSProperties["alignItems"];
const Flex = styled.div<{
auto?: boolean;
column?: boolean;
align?: AlignValues;
justify?: JustifyValues;
wrap?: boolean;
shrink?: boolean;
reverse?: boolean;
gap?: number;
}>`
display: flex;
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
flex-direction: ${({ column, reverse }) =>
reverse
? column
? "column-reverse"
: "row-reverse"
: column
? "column"
: "row"};
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
flex-wrap: ${({ wrap }) => (wrap ? "wrap" : "initial")};
flex-shrink: ${({ shrink }) =>
shrink === true ? 1 : shrink === false ? 0 : "initial"};
gap: ${({ gap }) => (gap ? `${gap}px` : "initial")};
min-height: 0;
min-width: 0;
`;
export default Flex; export default Flex;

View File

@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit"; import { PopoverDisclosure, usePopoverState } from "reakit";
import { MenuItem } from "reakit/Menu"; import { MenuItem } from "reakit/Menu";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections"; import { colorPalette } from "@shared/utils/collections";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton"; import NudeButton from "~/components/NudeButton";
@@ -10,7 +11,7 @@ import Text from "~/components/Text";
import useOnClickOutside from "~/hooks/useOnClickOutside"; import useOnClickOutside from "~/hooks/useOnClickOutside";
import lazyWithRetry from "~/utils/lazyWithRetry"; import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount"; import DelayedMount from "./DelayedMount";
import { IconLibrary } from "./Icons/IconLibrary"; import InputSearch from "./InputSearch";
import Popover from "./Popover"; import Popover from "./Popover";
const icons = IconLibrary.mapping; const icons = IconLibrary.mapping;
@@ -38,11 +39,12 @@ function IconPicker({
onChange, onChange,
className, className,
}: Props) { }: Props) {
const [query, setQuery] = React.useState("");
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const popover = usePopoverState({ const popover = usePopoverState({
gutter: 0, gutter: 0,
placement: "bottom", placement: "right",
modal: true, modal: true,
}); });
@@ -51,9 +53,15 @@ function IconPicker({
onOpen?.(); onOpen?.();
} else { } else {
onClose?.(); onClose?.();
setQuery("");
} }
}, [onOpen, onClose, popover.visible]); }, [onOpen, onClose, popover.visible]);
const filteredIcons = IconLibrary.findIcons(query);
const handleFilter = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value.toLowerCase());
};
const styles = React.useMemo( const styles = React.useMemo(
() => ({ () => ({
default: { default: {
@@ -92,6 +100,9 @@ function IconPicker({
{ capture: true } { capture: true }
); );
const iconNames = Object.keys(icons);
const delayPerIcon = 250 / iconNames.length;
return ( return (
<> <>
<PopoverDisclosure {...popover}> <PopoverDisclosure {...popover}>
@@ -112,69 +123,77 @@ function IconPicker({
</PopoverDisclosure> </PopoverDisclosure>
<Popover <Popover
{...popover} {...popover}
width={388} width={552}
aria-label={t("Choose icon")} aria-label={t("Choose an icon")}
hideOnClickOutside={false} hideOnClickOutside={false}
> >
<Icons> <Flex column gap={12}>
{Object.keys(icons).map((name, index) => ( <Text size="large" weight="xbold">
<MenuItem key={name} onClick={() => onChange(color, name)}> {t("Choose an icon")}
{(props) => ( </Text>
<IconButton <InputSearch
style={ value={query}
{ placeholder={`${t("Filter")}`}
"--delay": `${index * 8}ms`, onChange={handleFilter}
} as React.CSSProperties autoFocus
} />
{...props} <div>
> {iconNames.map((name, index) => (
<Icon <MenuItem key={name} onClick={() => onChange(color, name)}>
as={IconLibrary.getComponent(name)} {(props) => (
color={color} <IconButton
size={30} style={
{
opacity: query
? filteredIcons.includes(name)
? 1
: 0.3
: undefined,
"--delay": `${Math.round(index * delayPerIcon)}ms`,
} as React.CSSProperties
}
{...props}
> >
{initial} <Icon
</Icon> as={IconLibrary.getComponent(name)}
</IconButton> color={color}
)} size={30}
</MenuItem> >
))} {initial}
</Icons> </Icon>
<Colors> </IconButton>
<React.Suspense )}
fallback={ </MenuItem>
<DelayedMount> ))}
<Text>{t("Loading")}</Text> </div>
</DelayedMount> <Flex>
} <React.Suspense
> fallback={
<ColorPicker <DelayedMount>
color={color} <Text>{t("Loading")}</Text>
onChange={(color) => onChange(color.hex, icon)} </DelayedMount>
colors={colorPalette} }
triangle="hide" >
styles={styles} <ColorPicker
/> color={color}
</React.Suspense> onChange={(color) => onChange(color.hex, icon)}
</Colors> colors={colorPalette}
triangle="hide"
styles={styles}
/>
</React.Suspense>
</Flex>
</Flex>
</Popover> </Popover>
</> </>
); );
} }
const Icon = styled.svg` const Icon = styled.svg`
transition: fill 150ms ease-in-out; transition: color 150ms ease-in-out, fill 150ms ease-in-out;
transition-delay: var(--delay); transition-delay: var(--delay);
`; `;
const Colors = styled(Flex)`
padding: 8px;
`;
const Icons = styled.div`
padding: 8px;
`;
const IconButton = styled(NudeButton)` const IconButton = styled(NudeButton)`
vertical-align: top; vertical-align: top;
border-radius: 4px; border-radius: 4px;

View File

@@ -2,10 +2,10 @@ import { observer } from "mobx-react";
import { CollectionIcon } from "outline-icons"; import { CollectionIcon } from "outline-icons";
import { getLuminance } from "polished"; import { getLuminance } from "polished";
import * as React from "react"; import * as React from "react";
import { IconLibrary } from "@shared/utils/IconLibrary";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import { IconLibrary } from "./IconLibrary";
type Props = { type Props = {
/** The collection to show an icon for */ /** The collection to show an icon for */

View File

@@ -3,6 +3,7 @@ import { MoreIcon, QuestionMarkIcon, UserIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components"; import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission } from "@shared/types"; import { CollectionPermission } from "@shared/types";
import type Collection from "~/models/Collection"; import type Collection from "~/models/Collection";
import type Document from "~/models/Document"; import type Document from "~/models/Document";
@@ -14,7 +15,6 @@ import useStores from "~/hooks/useStores";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import { AvatarSize } from "../Avatar/Avatar"; import { AvatarSize } from "../Avatar/Avatar";
import CollectionIcon from "../Icons/CollectionIcon"; import CollectionIcon from "../Icons/CollectionIcon";
import Squircle from "../Squircle";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
import { StyledListItem } from "./MemberListItem"; import { StyledListItem } from "./MemberListItem";

View File

@@ -8,6 +8,7 @@ import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
import { UrlHelper } from "@shared/utils/UrlHelper"; import { UrlHelper } from "@shared/utils/UrlHelper";
import Document from "~/models/Document"; import Document from "~/models/Document";
@@ -21,7 +22,6 @@ import { AvatarSize } from "../Avatar/Avatar";
import CopyToClipboard from "../CopyToClipboard"; import CopyToClipboard from "../CopyToClipboard";
import NudeButton from "../NudeButton"; import NudeButton from "../NudeButton";
import { ResizingHeightContainer } from "../ResizingHeightContainer"; import { ResizingHeightContainer } from "../ResizingHeightContainer";
import Squircle from "../Squircle";
import Text from "../Text"; import Text from "../Text";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
import { StyledListItem } from "./MemberListItem"; import { StyledListItem } from "./MemberListItem";

View File

@@ -11,7 +11,7 @@ type Props = {
/** Whether the text should be selectable (defaults to false) */ /** Whether the text should be selectable (defaults to false) */
selectable?: boolean; selectable?: boolean;
/** The font weight of the text */ /** The font weight of the text */
weight?: "bold" | "normal"; weight?: "xbold" | "bold" | "normal";
/** Whether the text should be truncated with an ellipsis */ /** Whether the text should be truncated with an ellipsis */
ellipsis?: boolean; ellipsis?: boolean;
}; };
@@ -47,7 +47,9 @@ const Text = styled.span<Props>`
${(props) => ${(props) =>
props.weight && props.weight &&
css` css`
font-weight: ${props.weight === "bold" font-weight: ${props.weight === "xbold"
? 600
: props.weight === "bold"
? 500 ? 500
: props.weight === "normal" : props.weight === "normal"
? 400 ? 400

View File

@@ -61,6 +61,9 @@
"@dnd-kit/sortable": "^7.0.2", "@dnd-kit/sortable": "^7.0.2",
"@emoji-mart/data": "^1.0.6", "@emoji-mart/data": "^1.0.6",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@getoutline/y-prosemirror": "^1.0.18", "@getoutline/y-prosemirror": "^1.0.18",
"@hocuspocus/extension-throttle": "1.1.2", "@hocuspocus/extension-throttle": "1.1.2",
"@hocuspocus/provider": "1.1.2", "@hocuspocus/provider": "1.1.2",

View File

@@ -174,6 +174,7 @@ describe("#collections.move", () => {
token: admin.getJwtToken(), token: admin.getJwtToken(),
id: collection.id, id: collection.id,
index: "P", index: "P",
icon: "flame",
}, },
}); });
const body = await res.json(); const body = await res.json();
@@ -181,6 +182,20 @@ describe("#collections.move", () => {
expect(body.success).toBe(true); expect(body.success).toBe(true);
}); });
it("should return error when icon is not valid", async () => {
const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id });
const collection = await buildCollection({ teamId: team.id });
const res = await server.post("/api/collections.move", {
body: {
token: admin.getJwtToken(),
id: collection.id,
icon: "nonsRence",
},
});
expect(res.status).toEqual(400);
});
it("should return error when index is not valid", async () => { it("should return error when index is not valid", async () => {
const team = await buildTeam(); const team = await buildTeam();
const admin = await buildAdmin({ teamId: team.id }); const admin = await buildAdmin({ teamId: team.id });

View File

@@ -2,11 +2,20 @@ import isUndefined from "lodash/isUndefined";
import { z } from "zod"; import { z } from "zod";
import { randomElement } from "@shared/random"; import { randomElement } from "@shared/random";
import { CollectionPermission, FileOperationFormat } from "@shared/types"; import { CollectionPermission, FileOperationFormat } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections"; import { colorPalette } from "@shared/utils/collections";
import { Collection } from "@server/models"; import { Collection } from "@server/models";
import { ValidateColor, ValidateIcon, ValidateIndex } from "@server/validation"; import { ValidateColor, ValidateIndex } from "@server/validation";
import { BaseSchema } from "../schema"; import { BaseSchema } from "../schema";
function zodEnumFromObjectKeys<
TI extends Record<string, any>,
R extends string = TI extends Record<infer R, any> ? R : never
>(input: TI): z.ZodEnum<[R, ...R[]]> {
const [firstKey, ...otherKeys] = Object.keys(input) as [R, ...R[]];
return z.enum([firstKey, ...otherKeys]);
}
export const CollectionsCreateSchema = BaseSchema.extend({ export const CollectionsCreateSchema = BaseSchema.extend({
body: z.object({ body: z.object({
name: z.string(), name: z.string(),
@@ -20,12 +29,7 @@ export const CollectionsCreateSchema = BaseSchema.extend({
.nullish() .nullish()
.transform((val) => (isUndefined(val) ? null : val)), .transform((val) => (isUndefined(val) ? null : val)),
sharing: z.boolean().default(true), sharing: z.boolean().default(true),
icon: z icon: zodEnumFromObjectKeys(IconLibrary.mapping).optional(),
.string()
.max(ValidateIcon.maxLength, {
message: `Must be ${ValidateIcon.maxLength} or fewer characters long`,
})
.optional(),
sort: z sort: z
.object({ .object({
field: z.union([z.literal("title"), z.literal("index")]), field: z.union([z.literal("title"), z.literal("index")]),
@@ -171,12 +175,7 @@ export const CollectionsUpdateSchema = BaseSchema.extend({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string().optional(), name: z.string().optional(),
description: z.string().nullish(), description: z.string().nullish(),
icon: z icon: zodEnumFromObjectKeys(IconLibrary.mapping).nullish(),
.string()
.max(ValidateIcon.maxLength, {
message: `Must be ${ValidateIcon.maxLength} or fewer characters long`,
})
.nullish(),
permission: z.nativeEnum(CollectionPermission).nullish(), permission: z.nativeEnum(CollectionPermission).nullish(),
color: z color: z
.string() .string()

View File

@@ -246,7 +246,3 @@ export class ValidateColor {
public static regex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i; public static regex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
public static message = "Must be a hex value (please use format #FFFFFF)"; public static message = "Must be a hex value (please use format #FFFFFF)";
} }
export class ValidateIcon {
public static maxLength = 50;
}

View File

@@ -0,0 +1,38 @@
import { CSSProperties } from "react";
import styled from "styled-components";
type JustifyValues = CSSProperties["justifyContent"];
type AlignValues = CSSProperties["alignItems"];
const Flex = styled.div<{
auto?: boolean;
column?: boolean;
align?: AlignValues;
justify?: JustifyValues;
wrap?: boolean;
shrink?: boolean;
reverse?: boolean;
gap?: number;
}>`
display: flex;
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
flex-direction: ${({ column, reverse }) =>
reverse
? column
? "column-reverse"
: "row-reverse"
: column
? "column"
: "row"};
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
flex-wrap: ${({ wrap }) => (wrap ? "wrap" : "initial")};
flex-shrink: ${({ shrink }) =>
shrink === true ? 1 : shrink === false ? 0 : "initial"};
gap: ${({ gap }) => (gap ? `${gap}px` : "initial")};
min-height: 0;
min-width: 0;
`;
export default Flex;

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { s } from "@shared/styles"; import { s } from "../styles";
import Squircle from "../Squircle"; import Squircle from "./Squircle";
type Props = { type Props = {
/** The width and height of the icon, including standard padding. */ /** The width and height of the icon, including standard padding. */

View File

@@ -238,7 +238,8 @@
"{{authorName}} created <3></3>": "{{authorName}} created <3></3>", "{{authorName}} created <3></3>": "{{authorName}} created <3></3>",
"{{authorName}} opened <3></3>": "{{authorName}} opened <3></3>", "{{authorName}} opened <3></3>": "{{authorName}} opened <3></3>",
"Show menu": "Show menu", "Show menu": "Show menu",
"Choose icon": "Choose icon", "Choose an icon": "Choose an icon",
"Filter": "Filter",
"Loading": "Loading", "Loading": "Loading",
"Select a color": "Select a color", "Select a color": "Select a color",
"Search": "Search", "Search": "Search",
@@ -864,7 +865,6 @@
"Enterprise": "Enterprise", "Enterprise": "Enterprise",
"Recent imports": "Recent imports", "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.", "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", "Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
"Document updated": "Document updated", "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", "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

@@ -1,3 +1,92 @@
import {
faHeart,
faWandSparkles,
faUmbrella,
faMugHot,
faBook,
faDroplet,
faBrush,
faSnowflake,
faShop,
faShirt,
faBagShopping,
faGauge,
faMountainSun,
faPassport,
faPhoneVolume,
faNewspaper,
faNetworkWired,
faRocket,
faStarOfLife,
faSeedling,
faTrain,
faMicrochip,
faRecordVinyl,
faTrophy,
faHammer,
faRobot,
faCrown,
faCube,
faRoad,
faPuzzlePiece,
faIndustry,
faWorm,
faVault,
faUtensils,
faUserGraduate,
faUniversalAccess,
faTractor,
faTent,
faSpa,
faSocks,
faScissors,
faSailboat,
faPizzaSlice,
faPaw,
faMap,
faLaptopCode,
faKitMedical,
faFaceSurprise,
faFaceSmileWink,
faFaceSmileBeam,
faFaceMeh,
faFaceLaugh,
faFaceGrinStars,
faFaceDizzy,
faDog,
faCrow,
faCompactDisc,
faClapperboard,
faFeather,
faFish,
faCat,
faTree,
faShield,
faLaptop,
faDisplay,
faPrescription,
faWheelchairMove,
faGift,
faMagnet,
faPaintRoller,
faGamepad,
faCookieBite,
faTowerCell,
faTooth,
faDollarSign,
faSterlingSign,
faYenSign,
faPesoSign,
faRainbow,
faPenRuler,
faSwatchbook,
faStarAndCrescent,
faSolarPanel,
faUmbrellaBeach,
faGem,
faDna,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import intersection from "lodash/intersection"; import intersection from "lodash/intersection";
import { import {
BookmarkedIcon, BookmarkedIcon,
@@ -54,7 +143,21 @@ import {
ThumbsUpIcon, ThumbsUpIcon,
TruckIcon, TruckIcon,
} from "outline-icons"; } from "outline-icons";
import LetterIcon from "./LetterIcon"; import * as React from "react";
import styled from "styled-components";
import LetterIcon from "../components/LetterIcon";
type IconMapping = {
component: React.ComponentType<FAProps>;
keywords?: string;
};
type FAProps = {
color?: string;
size?: number;
className?: string;
style?: React.CSSProperties;
};
export class IconLibrary { export class IconLibrary {
/** /**
@@ -78,7 +181,7 @@ export class IconLibrary {
for (const key of keys) { for (const key of keys) {
const icon = this.mapping[key]; const icon = this.mapping[key];
const keywords = icon.keywords.split(" "); const keywords = icon.keywords?.split(" ");
const namewords = keyword.toLocaleLowerCase().split(" "); const namewords = keyword.toLocaleLowerCase().split(" ");
const matches = intersection(namewords, keywords); const matches = intersection(namewords, keywords);
@@ -90,11 +193,32 @@ export class IconLibrary {
return undefined; return undefined;
} }
/**
* Find icons.
*
* @param query The query to search for
* @returns The icon results.
*/
public static findIcons(query: string) {
return Object.keys(this.mapping)
.map((key) => {
const icon = this.mapping[key];
const keywords = `${icon.keywords ?? ""} ${key}`;
if (keywords.includes(query.toLocaleLowerCase())) {
return key;
}
return undefined;
})
.filter(Boolean);
}
/** /**
* A map of all icons available to end users in the app. This does not include icons that are used * A map of all icons available to end users in the app. This does not include icons that are used
* internally only, which can be imported from `outline-icons` directly. * internally only, which can be imported from `outline-icons` directly.
*/ */
public static mapping = { public static mapping: Record<string, IconMapping> = {
// Internal icons
academicCap: { academicCap: {
component: AcademicCapIcon, component: AcademicCapIcon,
keywords: "learn teach lesson guide tutorial onboarding training", keywords: "learn teach lesson guide tutorial onboarding training",
@@ -311,5 +435,123 @@ export class IconLibrary {
component: WarningIcon, component: WarningIcon,
keywords: "warning alert error", keywords: "warning alert error",
}, },
};
// Font awesome
...Object.fromEntries(
[
faHeart,
faWandSparkles,
faUmbrella,
faMugHot,
faBook,
faDroplet,
faBrush,
faSnowflake,
faShop,
faShirt,
faBagShopping,
faGauge,
faMountainSun,
faPassport,
faPhoneVolume,
faNewspaper,
faNetworkWired,
faRocket,
faStarOfLife,
faSeedling,
faTrain,
faMicrochip,
faRecordVinyl,
faTrophy,
faHammer,
faRobot,
faCrown,
faCube,
faRoad,
faPuzzlePiece,
faIndustry,
faWorm,
faVault,
faUtensils,
faUserGraduate,
faUniversalAccess,
faTractor,
faTent,
faSpa,
faSocks,
faScissors,
faSailboat,
faPizzaSlice,
faPaw,
faMap,
faLaptopCode,
faKitMedical,
faFaceSurprise,
faFaceSmileWink,
faFaceSmileBeam,
faFaceMeh,
faFaceLaugh,
faFaceGrinStars,
faFaceDizzy,
faDog,
faCrow,
faCompactDisc,
faClapperboard,
faFeather,
faFish,
faCat,
faTree,
faShield,
faLaptop,
faDisplay,
faPrescription,
faWheelchairMove,
faGift,
faMagnet,
faPaintRoller,
faGamepad,
faCookieBite,
faTowerCell,
faTooth,
faDollarSign,
faSterlingSign,
faYenSign,
faPesoSign,
faRainbow,
faPenRuler,
faSwatchbook,
faStarAndCrescent,
faSolarPanel,
faUmbrellaBeach,
faGem,
faDna,
].map((icon) => [
icon.iconName,
{
component: ({ color, size = 24, className, style }: FAProps) => (
<FontAwesomeWrapper size={size}>
<FontAwesomeIcon
style={{
width: 0.6666666667 * size,
height: 0.6666666667 * size,
...style,
}}
color={color}
icon={icon}
className={className}
/>
</FontAwesomeWrapper>
),
},
])
),
} as const;
} }
const FontAwesomeWrapper = styled.span<{ size: number }>`
display: flex;
align-items: center;
justify-content: center;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
`;

View File

@@ -110,22 +110,7 @@
lru-cache "^5.1.1" lru-cache "^5.1.1"
semver "^6.3.1" semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7", "@babel/helper-create-class-features-plugin@^7.22.15": "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7", "@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.24.1":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4"
integrity "sha1-l6YbOF5X/kWElvrRn45jtjyGfeQ= sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg=="
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-function-name" "^7.22.5"
"@babel/helper-member-expression-to-functions" "^7.22.15"
"@babel/helper-optimise-call-expression" "^7.22.5"
"@babel/helper-replace-supers" "^7.22.9"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.24.1":
version "7.24.1" version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz#db58bf57137b623b916e24874ab7188d93d7f68f" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz#db58bf57137b623b916e24874ab7188d93d7f68f"
integrity sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA== integrity sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==
@@ -248,16 +233,7 @@
"@babel/helper-wrap-function" "^7.18.9" "@babel/helper-wrap-function" "^7.18.9"
"@babel/types" "^7.18.9" "@babel/types" "^7.18.9"
"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.22.20", "@babel/helper-replace-supers@^7.22.9": "@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.22.20", "@babel/helper-replace-supers@^7.22.9", "@babel/helper-replace-supers@^7.24.1":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793"
integrity "sha1-4302cSPKmP5FWpiHc07S4W63p5M= sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="
dependencies:
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-member-expression-to-functions" "^7.22.15"
"@babel/helper-optimise-call-expression" "^7.22.5"
"@babel/helper-replace-supers@^7.24.1":
version "7.24.1" version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1"
integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==
@@ -624,20 +600,13 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript@^7.24.1": "@babel/plugin-syntax-typescript@^7.24.1", "@babel/plugin-syntax-typescript@^7.7.2":
version "7.24.1" version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844"
integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-syntax-typescript@^7.7.2":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f"
integrity "sha1-JPRgyF27yYPNK5xJlBeLzAHflY8= sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ=="
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-transform-arrow-functions@^7.18.6": "@babel/plugin-transform-arrow-functions@^7.18.6":
version "7.20.7" version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551"
@@ -1663,6 +1632,32 @@
dependencies: dependencies:
tslib "2.4.0" tslib "2.4.0"
"@fortawesome/fontawesome-common-types@6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz#eaf2f5699f73cef198454ebc0c414e3688898179"
integrity sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==
"@fortawesome/fontawesome-svg-core@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz#4b42de71e196039b0d5ccf88559b8044e3296c21"
integrity sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==
dependencies:
"@fortawesome/fontawesome-common-types" "6.5.2"
"@fortawesome/free-solid-svg-icons@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz#9b40b077b27400a5e9fcbf2d15b986c7be69e9ca"
integrity sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==
dependencies:
"@fortawesome/fontawesome-common-types" "6.5.2"
"@fortawesome/react-fontawesome@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4"
integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==
dependencies:
prop-types "^15.8.1"
"@getoutline/y-prosemirror@^1.0.18": "@getoutline/y-prosemirror@^1.0.18":
version "1.0.18" version "1.0.18"
resolved "https://registry.yarnpkg.com/@getoutline/y-prosemirror/-/y-prosemirror-1.0.18.tgz#17245c0362d30adb85131c86fb9a59358884b234" resolved "https://registry.yarnpkg.com/@getoutline/y-prosemirror/-/y-prosemirror-1.0.18.tgz#17245c0362d30adb85131c86fb9a59358884b234"