diff --git a/CHANGELOG.md b/CHANGELOG.md index bb962612..4b7140e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,12 @@ - [Flutter] Poligon Node support with XImage (svg) - [Lint] Primal naming & grouping linting for better code export quality. this is tracked sperately on [lint](https://github.com/bridgedxyz/lint) +## [2022.12.0.1] - 2022-12-6 (schduled) + +- New Icons set added. + - Unicon Icons + - Radix-ui Icons + ## [2022.4.0.3] - 2022-04-29 - New feature: Publish as website diff --git a/figma-core/event-handlers/create-icon.ts b/figma-core/event-handlers/create-icon.ts index 854ca25d..f68858b5 100644 --- a/figma-core/event-handlers/create-icon.ts +++ b/figma-core/event-handlers/create-icon.ts @@ -1,4 +1,3 @@ -import { NamedIconConfig } from "@reflect-ui/core"; import { EK_CREATE_ICON, EK_ICON_DRAG_AND_DROPPED } from "@core/constant"; import { PluginSdkService } from "@plugin-sdk/service"; import { IconPlacement, renderSvgIcon } from "../reflect-render/icons.render"; @@ -7,7 +6,12 @@ import { addEventHandler } from "../code-thread"; interface CreateIconProps { key: string; svg: string; - config: NamedIconConfig; + config: { + name: string; + size: number; + variant?: string; + package: string; + }; } function createIcon( diff --git a/figma-core/reflect-render/icons.render/index.ts b/figma-core/reflect-render/icons.render/index.ts index 668777c7..e63ae97b 100644 --- a/figma-core/reflect-render/icons.render/index.ts +++ b/figma-core/reflect-render/icons.render/index.ts @@ -18,7 +18,7 @@ export function renderSvgIcon( data: string, color: Color = "#000000", placement: IconPlacement = "center", - config?: NamedIconConfig + config?: { size: number; name: string; package: string; variant?: string } ): FrameNode { console.log(`inserting icon with name ${name} and data ${data}`); @@ -47,7 +47,7 @@ export function renderSvgIcon( // operate extra manipulation if config is available. if (config) { - const size = Number(config.default_size); + const size = Number(config.size); node.resize(size, size); } @@ -62,14 +62,19 @@ export function renderSvgIcon( export function buildReflectIconNameForRender( name: string, - config: NamedIconConfig + config: { name: string; package: string; variant?: string } ): string { - if (config.host == "material") { - return `icons/mdi_${name}`; - } else if (config.host == "ant-design") { - return `icons/antd-${name}`; - } else { - return `icons/${name}`; + switch (config.package) { + case "material": + return `icons/mdi_${name}`; + case "ant-design": + return `icons/antd-${name}`; + case "radix-ui": + return `icons/radix-${name}`; + case "unicons": + return `icons/unicons-${name}`; + default: + return `icons/${name}`; } } diff --git a/packages/app-icons-loader/history.ts b/packages/app-icons-loader/history.ts new file mode 100644 index 00000000..971f0465 --- /dev/null +++ b/packages/app-icons-loader/history.ts @@ -0,0 +1,43 @@ +// save recently used items + +import type { Icon } from "./resources"; + +const _k_store_key = "icons-load-history"; + +export class IconsLoadHistory { + private readonly data: Array = []; + + constructor(readonly max: number = 50) { + const items = localStorage.getItem(_k_store_key); + if (items) { + this.data = JSON.parse(items); + } + } + + list(to: number = Infinity): Array { + return Array.from(this.data).reverse().slice(0, to); + } + + push(item: Icon) { + const index = this.data.findIndex( + (i) => + i.package === item.package && + i.name === item.name && + i.variant === item.variant + ); + if (index >= 0) { + this.data.splice(index, 1); + } + this.data.push(item); + + if (this.data.length > this.max) { + this.data.shift(); + } + + this.save(); + } + + private save() { + localStorage.setItem(_k_store_key, JSON.stringify(this.data)); + } +} diff --git a/packages/app-icons-loader/icon-item.tsx b/packages/app-icons-loader/icon-item.tsx new file mode 100644 index 00000000..2070b2be --- /dev/null +++ b/packages/app-icons-loader/icon-item.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useRef, useState } from "react"; +import Tooltip from "@material-ui/core/Tooltip"; +import { + EK_CREATE_ICON, + EK_ICON_DRAG_AND_DROPPED, +} from "@core/constant/ek.constant"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import { Draggable } from "@plugin-sdk/draggable"; +import { assistant as analytics } from "@analytics.bridged.xyz/internal"; +import styled from "@emotion/styled"; +import { IconMeta, Icon, loadSvg, makeIconUrl, useIcons } from "./resources"; + +type IconItemProps = Icon & { onClick: () => void }; + +export function IconItem({ onClick, ...props }: IconItemProps) { + const { package: _package, name, variant } = props; + const [loading, setLoading] = useState(false); + const [loaded, setLoaded] = useState(false); + const [downloading, setDownloading] = useState(false); + + const _aid = name + " " + variant; // id for analytics + + useEffect(() => { + setTimeout(() => { + setLoading(!loaded); + }, 5); + }, [loaded]); + + const _onUserLoadIconToCanvas = () => { + // ANALYTICS + analytics.event_load_icon({ + icon_name: _aid, + }); + }; + + async function loadData() { + _onUserLoadIconToCanvas(); + try { + setDownloading(true); + const svg = await loadSvg(props, { + disable_cache: true, + }); + const data = { + key: name, + svg: svg, + config: { + name: props.name, + variant: props.variant, + size: props.size, + package: props.package, + }, + }; + return data; + } catch (_) { + throw _; + } finally { + setDownloading(false); + } + } + + const onclick = () => { + onClick(); + _onUserLoadIconToCanvas(); + loadData().then((d) => { + parent.postMessage( + { + pluginMessage: { + type: EK_CREATE_ICON, + data: d, + }, + }, + "*" + ); + }); + }; + + return ( + + + + {downloading ? ( + + ) : ( + + setLoaded(true)} + width="24" + height="24" + /> + + )} + + + + ); +} + +const IconButton = styled.button` + background-color: #fff; + border: none; + height: 48px !important; + width: 48px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: #eeeeee; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: 0.25s; + } +`; diff --git a/packages/app-icons-loader/icons-loader.tsx b/packages/app-icons-loader/icons-loader.tsx index 49726e71..a1739585 100644 --- a/packages/app-icons-loader/icons-loader.tsx +++ b/packages/app-icons-loader/icons-loader.tsx @@ -1,35 +1,47 @@ -import React, { useEffect, useRef, useState } from "react"; -import { icons } from "@base-sdk/resources"; -import { NamedDefaultOssIconConfig } from "@reflect-ui/core"; -import Tooltip from "@material-ui/core/Tooltip"; +import React, { + useMemo, + useEffect, + useRef, + useState, + useCallback, +} from "react"; +import styled from "@emotion/styled"; import GridList from "@material-ui/core/GridList"; import GridListTile from "@material-ui/core/GridListTile"; -import { - EK_CREATE_ICON, - EK_ICON_DRAG_AND_DROPPED, -} from "@core/constant/ek.constant"; import LinearProgress from "@material-ui/core/LinearProgress"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import { withStyles, Select, MenuItem, InputBase } from "@material-ui/core"; -import { Search } from "@material-ui/icons"; -const CONFIG_JSON_S3 = - "https://reflect-icons.s3-us-west-1.amazonaws.com/all.json"; -import { Draggable } from "@plugin-sdk/draggable"; -import { assistant as analytics } from "@analytics.bridged.xyz/internal"; -import styled from "@emotion/styled"; +import { Icon, useIcons, IConStyleVariant } from "./resources"; +import { IconItem } from "./icon-item"; +import InfiniteScroll from "react-infinite-scroller"; +import { debounce } from "./utils"; +import { IconSearch } from "./icons-search"; +import { IconsLoadHistory } from "./history"; +import _ from "lodash"; + +function useRecentlyUsedIcons(max: number = 20) { + const [recentIcons, setRecentIcons] = useState([]); + const h = useMemo(() => new IconsLoadHistory(max), []); + + useEffect(() => { + setRecentIcons(h.list()); + }, [h]); + + const addHistory = useCallback( + (icon: Icon) => { + h.push(icon); + setRecentIcons(h.list()); + }, + [h] + ); + + return [recentIcons, addHistory] as const; +} -// cached icons configs -let CONFIGS: Map; export function IconsLoader() { - const [configs, setConfigs] = useState< - Map - >(undefined); - const [queryTerm, setQueryTerm] = useState(undefined); - const [iconLoadLimit, setIconLoadLimit] = useState(100); - const iconRef = useRef(undefined); + const [query, setQuery] = useState(undefined); + const [max, setMax] = useState(100); const [iconProperty, setIconProperty] = useState<{ default_size: string; - variant: string; + variant: IConStyleVariant | "variant"; host: string; }>({ default_size: "size", @@ -37,415 +49,129 @@ export function IconsLoader() { host: "material", }); - useEffect(() => { - if (CONFIGS) { - setConfigs(CONFIGS); - } else { - fetch(CONFIG_JSON_S3) - .then((response) => response.json()) - .then((json) => { - CONFIGS = json; - setConfigs(json); - }); - } - }, []); + const [recentlyUsedIcons, addHistory] = useRecentlyUsedIcons(); + const { icons, hasMore } = useIcons({ + max: max, + query: query, + }); useEffect(() => { - setIconLoadLimit(100); - }, [iconProperty, queryTerm]); - - function IconList(props: { icons: [string, NamedDefaultOssIconConfig][] }) { - const { icons } = props; + // reset max to 100 + setMax(100); + }, [iconProperty, query]); - return ( - <> - {/* */} - - {icons.map((i) => { - const key = i[0]; - const config = i[1]; - return ( - - - - ); - })} - - {/* */} - - ); - } - - let list; - if (configs) { - const validQueryTerm = queryTerm !== undefined && queryTerm.length >= 1; - // const searchOnlyDefaults: boolean = !validQueryTerm - const defaultIcons = filterIcons(configs, { - includes: validQueryTerm ? queryTerm : undefined, - }).reduce((acc: any[], cur: any) => { - if ( - iconProperty.default_size === "size" && - iconProperty.variant === "variant" - ) { - acc.push(cur); - } else if ( - iconProperty.default_size === "size" && - cur[1].variant === iconProperty.variant - ) { - acc.push(cur); - } else if ( - iconProperty.variant === "variant" && - cur[1].default_size === iconProperty.default_size - ) { - acc.push(cur); - } else if ( - cur[1].default_size === iconProperty.default_size && - cur[1].variant === iconProperty.variant - ) { - acc.push(cur); - } - return acc; - }, []); - - const icons = sort_icon(defaultIcons as [string, any]).slice( - 0, - iconLoadLimit - ); - list = ; - } else { - list = ; - } - - const handleScroll = (e) => { - const scrollHeight = document.documentElement.scrollHeight; - const scrollTop = document.documentElement.scrollTop; - const clientHeight = document.documentElement.clientHeight; - if (scrollTop + clientHeight >= scrollHeight - 200) { - setIconLoadLimit(iconLoadLimit + 100); - } + const onIconClick = (icon: Icon) => { + addHistory(icon); }; - useEffect(() => { - window.addEventListener("scroll", handleScroll); - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, [handleScroll]); + const loading = icons === undefined; + const do_show_recently_used = recentlyUsedIcons?.length > 0 && !query; + + // group by package + const grouped = _.groupBy(icons ?? [], (d) => d.package); return ( <> - <>{list} - - ); -} - -function IconSearch(props: { - onChange: (value: string) => void; - onSelectIconProperty: (value: any) => void; -}) { - const iconPropertyList = { - default_size: ["Size", "16", "20", "24", "28", "32"], - variant: ["Variant", "Outlined", "Twotone", "Default", "Sharp"], - }; - const [iconProperty, setIconProperty] = useState({ - default_size: "Size", - variant: "Variant", - }); - - const BootstrapInput = withStyles((theme) => ({ - root: { - "label + &": { - marginTop: theme.spacing(3), - }, - }, - input: { - fontSize: 14, - }, - }))(InputBase); - - const onSelectValue = (type: string, value: any) => { - if (type === "size") { - props.onSelectIconProperty((d) => ({ - ...d, - default_size: value.toLocaleLowerCase(), - })); - setIconProperty((d) => ({ - ...d, - default_size: value, - })); - } else if (type === "variant") { - props.onSelectIconProperty((d) => ({ - ...d, - variant: value.toLocaleLowerCase(), - })); - setIconProperty((d) => ({ - ...d, - variant: value, - })); - } - }; - - return ( - - - - props.onChange(e.target.value.toLocaleLowerCase())} + <> + - - - - onSelectValue("variant", e.target.value)} - input={} - > - {iconPropertyList.variant.map((i) => ( - {i} - ))} - - - - onSelectValue("size", e.target.value)} - input={} - > - {iconPropertyList.default_size.map((i) => ( - - {i === "Size" ? "Size" : i + " x " + i} - - ))} - - - - - ); -} - -function filterIcons( - configs: Map, - options: { - includes?: string; - } -): [string, NamedDefaultOssIconConfig][] { - const keys = Object.keys(configs); - const defaultIcons = keys - .map<[string, NamedDefaultOssIconConfig]>((k) => { - const item = configs[k] as NamedDefaultOssIconConfig; - if (options.includes) { - if (k.includes(options.includes)) { - return [k, item]; - } else { - return; - } - } - return [k, item]; - }) - .filter((k) => k !== undefined); - console.log("default icons loaded", defaultIcons.length); - return defaultIcons; + { + setMax((d) => d + 100); + }} + > + + {do_show_recently_used && ( +
+
Frequently used
+ +
+ )} + + {Object.keys(grouped).map((key) => { + const icons = grouped[key]; + return ( +
+
{key}
+ +
+ ); + })} + {/* {icons?.length > 0 && ( + + )} */} +
+
+ + + ); } -function IconItem(props: { name: string; config: NamedDefaultOssIconConfig }) { - const { name, config } = props; - const [downloading, setDownloading] = useState(false); - - const _onUserLoadIconToCanvas = () => { - // ANALYTICS - analytics.event_load_icon({ - icon_name: props.name, - }); - }; - - async function loadData() { - _onUserLoadIconToCanvas(); - try { - setDownloading(true); - const svg = await icons.loadSvg(name, config, { - disable_cache: true, - }); - const data = { - key: name, - svg: svg, - config: config, - }; - return data; - } catch (_) { - throw _; - } finally { - setDownloading(false); - } - } - - const onClick = () => { - _onUserLoadIconToCanvas(); - loadData().then((d) => { - parent.postMessage( - { - pluginMessage: { - type: EK_CREATE_ICON, - data: d, - }, - }, - "*" - ); - }); - }; - +const IconList = React.forwardRef(function ( + { + icons, + onIconClick, + }: { + icons: Icon[]; + onIconClick?: (icon: Icon) => void; + }, + ref: any +) { return ( - - + - - {downloading ? ( - - ) : ( - - { + const { package: _p, name, variant } = icon; + return ( + + { + onIconClick?.(icon); + }} /> - - )} - - - + + ); + })} + + ); -} - -/** - * sorts icons with below logic - * 1. sort a-z first - * 2. sort number later - * - * e.g. ["a", "b", "1", "2", "1a", "a1"] - * -> ["a", "a1", "b", "1", "1a", "2"] - */ -function sort_icon(icons: [string, any]) { - const _contains_number_char = (c) => { - return "0123456789".includes(c[0] ?? ""); - }; - return icons.sort((_i, _i2) => { - const i = _i[0]; - const i2 = _i2[0]; - if (_contains_number_char(i) && _contains_number_char(i2)) { - return Number(i) - Number(i2); - } else if (_contains_number_char(i) && !_contains_number_char(i2)) { - return 1; - } else if (!_contains_number_char(i) && _contains_number_char(i2)) { - return -1; - } else if (!_contains_number_char(i) && !_contains_number_char(i2)) { - if (i < i2) { - return -1; - } - if (i > i2) { - return 1; - } - return 0; - } - }); -} +}); -const ListWrap = styled.div` +const Section = styled.div` display: flex; - flex-wrap: wrap; -`; - -const Wrapper = styled.div` - padding-top: 14px; - padding-bottom: 15px; - position: relative; -`; - -const SearchBar = styled.div` + flex-direction: column; width: 100%; - font-size: 14px; - height: 55px; - padding: 8px; - display: flex; - align-items: center; - - svg { - margin: 10px 10px 10px 8px; - font-size: 20px; + margin-bottom: 20px; + .title { + margin: 0; + padding: 16px; + padding-right: 0; + font-size: 12px; + font-weight: normal; + color: rgba(0, 0, 0, 0.54); + text-transform: uppercase; } `; -const Input = styled.input` - width: 100%; - height: 90%; - border: none; - outline: none; - - font-size: 14px; - font-weight: 400; - line-height: 17px; - color: #adaeb2; - - &::placeholder { - color: #adaeb2; - } -`; - -const SearchChecker = styled.div` - height: 55px; - width: 100%; - display: flex; -`; - -const TypeCheck = styled.div` - flex: 2; - display: flex; - align-items: center; - justify-content: space-between; - font-size: 14px; - cursor: pointer; - padding: 0px 16px; -`; - -const StyledSelect = styled(Select)` - width: 100% !important; - &.root { - font-size: 14px; - font-weight: 400; - line-height: 17px; - } -`; - -const SizeCheck = styled.div` - flex: 1; +const ListWrap = styled.div` display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - padding: 0px 16px; + flex-wrap: wrap; `; const StyledLinearProgress = styled(LinearProgress)` @@ -457,7 +183,7 @@ const StyledLinearProgress = styled(LinearProgress)` } &.barColorPrimary { - background-color: #2562ff; + background-color: black; } `; @@ -471,22 +197,3 @@ const GridItem = styled(GridListTile)` height: auto; } `; - -const IconButton = styled.button` - background-color: #fff; - border: none; - height: 48px !important; - width: 48px; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: #eeeeee; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - transition: 0.25s; - } -`; diff --git a/packages/app-icons-loader/icons-search.tsx b/packages/app-icons-loader/icons-search.tsx new file mode 100644 index 00000000..0cfe4e24 --- /dev/null +++ b/packages/app-icons-loader/icons-search.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useRef, useState } from "react"; +import styled from "@emotion/styled"; +import { withStyles, Select, MenuItem, InputBase } from "@material-ui/core"; +import { Search } from "@material-ui/icons"; + +export function IconSearch(props: { + onChange: (value: string) => void; + onSelectIconProperty: (value: any) => void; +}) { + // const iconPropertyList = { + // default_size: ["Size", "16", "20", "24", "28", "32"], + // variant: ["Variant", "Outlined", "Twotone", "Default", "Sharp"], + // }; + // const [iconProperty, setIconProperty] = useState({ + // default_size: "Size", + // variant: "Variant", + // }); + + // const BootstrapInput = withStyles((theme) => ({ + // root: { + // "label + &": { + // marginTop: theme.spacing(3), + // }, + // }, + // input: { + // fontSize: 14, + // }, + // }))(InputBase); + + // const onSelectValue = (type: string, value: any) => { + // if (type === "size") { + // props.onSelectIconProperty((d) => ({ + // ...d, + // default_size: value.toLocaleLowerCase(), + // })); + // setIconProperty((d) => ({ + // ...d, + // default_size: value, + // })); + // } else if (type === "variant") { + // props.onSelectIconProperty((d) => ({ + // ...d, + // variant: value.toLocaleLowerCase(), + // })); + // setIconProperty((d) => ({ + // ...d, + // variant: value, + // })); + // } + // }; + + return ( + + + + props.onChange(e.target.value.toLocaleLowerCase())} + /> + + {/* + + onSelectValue("variant", e.target.value)} + input={} + > + {iconPropertyList.variant.map((i) => ( + + {i} + + ))} + + + + onSelectValue("size", e.target.value)} + input={} + > + {iconPropertyList.default_size.map((i) => ( + + {i === "Size" ? "Size" : i + " x " + i} + + ))} + + + */} + + ); +} + +const ControlsWrapper = styled.div` + padding-top: 14px; + padding-bottom: 15px; + position: relative; +`; + +const SearchBar = styled.div` + width: 100%; + font-size: 14px; + height: 55px; + padding: 8px; + display: flex; + align-items: center; + + svg { + margin: 10px 10px 10px 8px; + font-size: 20px; + } +`; + +const Input = styled.input` + width: 100%; + height: 90%; + border: none; + outline: none; + + font-size: 14px; + font-weight: 400; + line-height: 17px; + color: #adaeb2; + + &::placeholder { + color: #adaeb2; + } +`; + +const Filters = styled.div` + height: 55px; + width: 100%; + display: flex; +`; + +const TypeCheck = styled.div` + flex: 2; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + cursor: pointer; + padding: 0px 16px; +`; + +const StyledSelect = styled(Select)` + width: 100% !important; + &.root { + font-size: 14px; + font-weight: 400; + line-height: 17px; + } +`; + +const SizeCheck = styled.div` + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 0px 16px; +`; diff --git a/packages/app-icons-loader/package.json b/packages/app-icons-loader/package.json index d76b6460..b184433e 100644 --- a/packages/app-icons-loader/package.json +++ b/packages/app-icons-loader/package.json @@ -4,5 +4,10 @@ "authors": "Grida.co", "version": "0.0.0", "private": false, - "dependencies": {} -} \ No newline at end of file + "dependencies": { + "react-infinite-scroller": "^1.2.6" + }, + "devDependencies": { + "@types/react-infinite-scroller": "^1.2.3" + } +} diff --git a/packages/app-icons-loader/resources.ts b/packages/app-icons-loader/resources.ts new file mode 100644 index 00000000..44efbcc8 --- /dev/null +++ b/packages/app-icons-loader/resources.ts @@ -0,0 +1,253 @@ +import { useState, useEffect } from "react"; +import Axios from "axios"; +import { search } from "./search"; +const _BUCKET = "https://reflect-icons.s3-us-west-1.amazonaws.com"; +const _META_JSON_S3 = _BUCKET + "/all.v2.json"; + +export type IConStyleVariant = + | "solid" + | "outlined" + | "thin" + | "twotone" + | "sharp" + | "round"; + +export interface IconMeta { + uri: string; + name: string; + variants: Array; + font?: string; + codepoint?: number; + size: number; + package: string; + version: number; + category: string; + tags: string[]; +} + +export interface Icon { + name: string; + size: number; + variant?: IConStyleVariant | null; + package: string; +} + +let _cachedMeta: Array = undefined; +export function fetchMeta() { + if (_cachedMeta) { + return Promise.resolve(_cachedMeta); + } + return fetch(_META_JSON_S3) + .then((response) => response.json()) + .then((json) => { + _cachedMeta = json; + return json; + }); +} + +export function useIcons({ + query = "", + max = 100, +}: { + query?: string; + max?: number; +}) { + const [meta, setMeta] = useState>(undefined); + const [queriedMeta, setQueriedMeta] = useState>(undefined); + const [icons, setIcons] = useState>(undefined); + + useEffect(() => { + fetchMeta().then((d) => { + setMeta(d); + setQueriedMeta(d); + }); + }, []); + + useEffect(() => { + if (meta) { + setQueriedMeta(search(meta, { query })); + } + }, [query, meta]); + + useEffect(() => { + if (queriedMeta) { + let icons = queriedMeta.flatMap((meta) => { + if (meta.variants.length === 0) { + return { + name: meta.name, + size: meta.size, + package: meta.package, + } as Icon; + } + return meta.variants.map((variant) => { + return { + name: meta.name, + variant: variant, + size: meta.size, + package: meta.package, + } as Icon; + }); + }); + + setIcons(icons); + } + }, [queriedMeta]); + + const hasMore = icons?.length > max; + + let result = Array.from(icons ?? []); + + // sort by package name + + const pkg_order = query + ? ["radix-ui", "unicons", "material", "ant-design"] // if query is set, prioritize radix-ui and unicons + : ["radix-ui", "material", "unicons", "ant-design"]; // if query is not set, prioritize radix-ui and material + result = result.sort((i, i2) => { + const pkg1 = pkg_order.indexOf(i.package); + const pkg2 = pkg_order.indexOf(i2.package); + if (pkg1 < pkg2) { + return -1; + } + if (pkg1 > pkg2) { + return 1; + } + return 0; + }); + + return { icons: result.slice(0, max), hasMore }; +} + +/** + * sorts icons with below logic + * 1. sort a-z first + * 2. sort number later + * + * e.g. ["a", "b", "1", "2", "1a", "a1"] + * -> ["a", "a1", "b", "1", "1a", "2"] + */ +function sort_alphabetically(names: string[]) { + const _contains_number_char = (c) => { + return "0123456789".includes(c[0] ?? ""); + }; + return names.sort((i, i2) => { + if (_contains_number_char(i) && _contains_number_char(i2)) { + return Number(i) - Number(i2); + } else if (_contains_number_char(i) && !_contains_number_char(i2)) { + return 1; + } else if (!_contains_number_char(i) && _contains_number_char(i2)) { + return -1; + } else if (!_contains_number_char(i) && !_contains_number_char(i2)) { + if (i < i2) { + return -1; + } + if (i > i2) { + return 1; + } + return 0; + } + }); +} + +export async function loadSvg( + icon: Icon, + options?: { + disable_cache: boolean; + } +): Promise { + const headers = {}; + if (options?.disable_cache) { + headers["Cache-Control"] = "no-cache"; + } + + const url = makeIconUrl(icon); + const raw = + await // s3 cors issue. fetching resource wil cause cors issue with 200, if cache is enabled. (don't know why !) + ( + await Axios.get(url, { headers: headers }) + ).data; + return raw; +} + +export function makeIconUrl(icon: Icon): string { + const __ = sepkeymap[icon.package]; + let filename = icon.name; + if (icon.variant) { + switch (icon.package) { + case "material": { + switch (icon.variant) { + case "solid": + // on material, no suffix on solid + break; + case "outlined": + filename += `${__}outlined`; + break; + case "round": + filename += `${__}round`; + break; + case "sharp": + filename += `${__}sharp`; + break; + case "twotone": + filename += `${__}twotone`; + break; + } + break; + } + case "ant-design": { + switch (icon.variant) { + case "solid": + filename += `${__}default`; // legacy file naming support + break; + case "outlined": + filename += `${__}outlined`; + break; + case "round": + filename += `${__}round`; + break; + case "sharp": + filename += `${__}sharp`; + break; + case "twotone": + filename += `${__}twotone`; + break; + } + break; + } + case "radix-ui": { + // no variants supported on radix-ui icons + break; + } + case "unicons": { + switch (icon.variant) { + case "solid": + filename += `${__}solid`; + break; + case "thin": + filename += `${__}thinline`; // unicons only + break; + case "outlined": + filename += `${__}outlined`; + break; + case "round": + filename += `${__}round`; + break; + case "sharp": + filename += `${__}sharp`; + break; + case "twotone": + filename += `${__}twotone`; + break; + } + break; + } + } + } + return `https://reflect-icons.s3-us-west-1.amazonaws.com/${icon.package}/${filename}.svg`; +} + +const sepkeymap = { + material: "_", + "ant-design": "-", + "radix-ui": "-", + unicons: "-", +} as const; diff --git a/packages/app-icons-loader/search.ts b/packages/app-icons-loader/search.ts new file mode 100644 index 00000000..ce839f3a --- /dev/null +++ b/packages/app-icons-loader/search.ts @@ -0,0 +1,46 @@ +import type { IconMeta } from "./resources"; + +/** + * filters icon meta by query from name, category, tags + * @returns + */ +export function search( + metas: Array, + { query }: { query?: string } +): IconMeta[] { + // fields + // - name (fuzzy search) + // - tags (fuzzy search) + // - category (fuzzy search) + + if (!query) { + return metas; + } + + const q = query?.toLowerCase(); + // split [" ", ","] + const qs = q?.split(/[\s,]+/).filter((q) => q.length > 0); + + return metas.filter((m) => { + // name + if (qs.some((q) => m.name.toLowerCase().includes(q))) { + return true; + } + + // category + if (m.category && qs.some((q) => m.category.toLowerCase().includes(q))) { + return true; + } + + // package + if (qs.some((q) => m.package.toLowerCase().includes(q))) { + return true; + } + + // tags + if (m.tags.some((t) => qs.some((q) => t.toLowerCase().includes(q)))) { + return true; + } + // return m.tags.some((t) => t.includes(query)); + }); +} diff --git a/packages/app-icons-loader/utils.ts b/packages/app-icons-loader/utils.ts new file mode 100644 index 00000000..aa88e1bd --- /dev/null +++ b/packages/app-icons-loader/utils.ts @@ -0,0 +1,8 @@ +export const debounce = (func: any, wait: number) => { + let timeout: any; + return function (this: any, ...args: any[]) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +}; diff --git a/yarn.lock b/yarn.lock index 0780d066..94f61d7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4122,6 +4122,13 @@ dependencies: "@types/react" "^16" +"@types/react-infinite-scroller@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.3.tgz#b8dcb0e5762c3f79cc92e574d2c77402524cab71" + integrity sha512-l60JckVoO+dxmKW2eEG7jbliEpITsTJvRPTe97GazjF5+ylagAuyYdXl8YY9DQsTP9QjhqGKZROknzgscGJy0A== + dependencies: + "@types/react" "*" + "@types/react-native@^0.66.16": version "0.66.16" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.66.16.tgz#fa654d7a611f8c74122b5706958224ebadc82044" @@ -11854,6 +11861,15 @@ prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +prop-types@^15.5.8: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -12170,6 +12186,13 @@ react-helmet-async@^1.0.7: react-fast-compare "^3.2.0" shallowequal "^1.1.0" +react-infinite-scroller@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz#8b80233226dc753a597a0eb52621247f49b15f18" + integrity sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ== + dependencies: + prop-types "^15.5.8" + react-inspector@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8" @@ -12189,7 +12212,7 @@ react-is@17.0.2, "react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0 resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==