Figma plugin! (#227)

* initialize figma plugin

* Add icons to view

* Make the plugin work

* Fix search hook

* Finialize figma plugin

* Finish up icons

* remove unused code

* Add cover and icon
This commit is contained in:
Eric Fennis
2021-02-26 16:35:18 +01:00
committed by GitHub
parent c4f50417d5
commit 87ab0bfb62
21 changed files with 4655 additions and 11 deletions

View File

@@ -0,0 +1,44 @@
import { jsx } from '@emotion/core'
import theme from '../theme'
import { renderToString } from 'react-dom/server'
import { FC } from 'react';
interface IconButtonProps {
name: string,
component: FC,
}
function IconButton({ name, component: IconComponent }: IconButtonProps) {
const onIconclick = () => {
const svg = renderToString(<IconComponent/>);
parent.postMessage({ pluginMessage: { name, svg }}, '*')
}
return (
<button
key={name}
aria-label={name}
onClick={onIconclick}
css={{
padding: theme.space[2],
color: '#333',
background: 'transparent',
border: 0,
borderRadius: theme.radii[1],
appearance: 'none',
outline: 0,
'&:hover': {
background: 'rgba(0, 0, 0, 0.06)',
},
'&:focus, &:active': {
boxShadow: `inset 0 0 0 2px ${theme.colors.blue}`,
},
}}
>
<IconComponent />
</button>
)
}
export default IconButton

View File

@@ -0,0 +1,24 @@
import { createElement, forwardRef } from 'react'
const SearchIcon = forwardRef((props: any, ref) => createElement(
'svg',
{
xmlns: "http://www.w3.org/2000/svg",
width: 32,
height: 32,
clipRule: 'evenodd',
fillRule: 'evenodd',
ref,
...props,
},
[
createElement(
'path', {
d: 'm20 15c0 2.7614-2.2386 5-5 5s-5-2.2386-5-5 2.2386-5 5-5 5 2.2386 5 5zm-1.1256 4.5815c-1.0453.8849-2.3975 1.4185-3.8744 1.4185-3.3137 0-6-2.6863-6-6s2.6863-6 6-6 6 2.6863 6 6c0 1.4769-.5336 2.8291-1.4185 3.8744l4.2721 4.272-.7072.7072z',
key: 'path'
}
)
]
))
export default SearchIcon

View File

@@ -0,0 +1,43 @@
import { jsx } from '@emotion/core'
import theme from '../theme'
import SearchIcon from './search-icon'
interface SearchInputProps extends React.HTMLProps<HTMLDivElement> {
value: string,
iconCount: number,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
}
function SearchInput({ value, onChange, iconCount, ...props }: SearchInputProps) {
return (
<div css={{ position: 'relative' }} {...props}>
<div
css={{
position: 'absolute',
top: 0,
left: 0,
padding: theme.space[1],
}}
>
<SearchIcon fill="#333" />
</div>
<input
autoFocus
type="search"
value={value}
onChange={onChange}
placeholder={`Search ${iconCount} icons`}
css={{
width: '100%',
height: 40,
padding: `0 ${theme.space[4]} 0 36px`,
fontFamily: 'inherit',
fontSize: theme.fontSizes[0],
border: 0,
outline: 0,
}}
/>
</div>
)
}
export default SearchInput

View File

@@ -0,0 +1,21 @@
/**
* Converts string to camelcase
*
* @param {string} string
*/
export const toCamelCase = (string: string) =>
string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) =>
p2 ? p2.toUpperCase() : p1.toLowerCase(),
);
/**
* Converts string to PascalCase
*
* @param {string} string
*/
export const toPascalCase = (string: string) => {
const camelCase = toCamelCase(string);
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
};

View File

@@ -0,0 +1,9 @@
figma.showUI(__html__, { width: 300, height: 400 })
figma.ui.onmessage = ({name, svg}) => {
const icon = figma.createNodeFromSvg(svg)
icon.name = name
icon.x = figma.viewport.center.x
icon.y = figma.viewport.center.y
figma.currentPage.selection = [icon]
}

View File

@@ -0,0 +1,8 @@
export default {
space: [0, 4, 8, 12, 16],
fontSizes: [12, 14, 16],
colors: {
blue: '#18a0fb',
},
radii: [0, 2],
}

View File

@@ -0,0 +1,4 @@
declare module 'lucide-react';
declare module 'lucide';
declare module 'lucide/icons';
declare module 'lucide/build/icons';

View File

@@ -0,0 +1,10 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.9')
format('woff2'),
url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.9')
format('woff');
}

View File

@@ -0,0 +1 @@
<div id="root"></div>

View File

@@ -0,0 +1,82 @@
import { Global, jsx } from '@emotion/core'
import { version } from '../package.json'
import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'
import IconButton from './components/icon-button'
import SearchInput from './components/search-input'
import theme from './theme'
import './ui.css'
import tags from '../../../tags.json'
import * as iconComponents from 'lucide-react'
import { toPascalCase } from './helpers/naming';
import useSearch from '../../../site/src/lib/useSearch';
declare var ICONS: [];
function App() {
const [query, setQuery] = React.useState('')
const icons = ICONS.map(name => {
const componentName = toPascalCase(name);
return {
name,
tags: tags[name] || [],
component: iconComponents[componentName] || null
}
}).filter(({component}) => !!component)
const searchResults = useMemo(() => useSearch(icons, query), [icons, query])
return (
<div>
<Global
styles={{ body: { margin: 0, fontFamily: 'Inter, sans-serif' } }}
/>
<SearchInput
value={query}
iconCount={icons.length}
onChange={event => setQuery(event.target.value)}
css={{
position: 'sticky',
top: 0,
borderBottom: '1px solid #e5e5e5',
backfaceVisibility: 'hidden'
}}
/>
<div css={{ padding: theme.space[2] }}>
<div
css={{
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gridGap: theme.space[1],
}}
>
{searchResults.map(({name, component: Icon} :any) => (
<IconButton
name={name}
key={name}
component={Icon}
/>
))}
</div>
<div
css={{
marginTop: theme.space[2],
padding: theme.space[2],
fontSize: theme.fontSizes[0],
color: 'rgba(0, 0, 0, 0.5)',
}}
>
<a
href="https://lucide.dev"
target="_blank"
css={{ color: 'inherit' }}
>
Lucide v{version}
</a>
</div>
</div>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))