├── .eslintrc ├── .gitignore ├── .npmignore ├── .storybook ├── main.js └── preview.js ├── .vscode └── extensions.json ├── CHANGELOG.md ├── README.md ├── index.d.ts ├── package.json ├── src ├── components │ ├── Button │ │ ├── Button.module.scss │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ └── index.ts │ ├── Checkbox │ │ ├── Checkbox.module.scss │ │ ├── Checkbox.stories.tsx │ │ ├── Checkbox.tsx │ │ └── index.ts │ ├── IconButton │ │ ├── IconButton.module.scss │ │ ├── IconButton.stories.tsx │ │ ├── IconButton.tsx │ │ └── index.ts │ ├── Input │ │ ├── Input.module.scss │ │ ├── Input.stories.tsx │ │ ├── Input.tsx │ │ └── index.ts │ ├── InputLabel │ │ ├── InputLabel.module.scss │ │ ├── InputLabel.stories.tsx │ │ ├── InputLabel.tsx │ │ └── index.ts │ ├── OptionMenu │ │ ├── OptionMenu.module.scss │ │ ├── OptionMenu.stories.tsx │ │ ├── OptionMenu.tsx │ │ └── index.ts │ ├── Section │ │ ├── Section.module.scss │ │ ├── Section.stories.tsx │ │ ├── Section.tsx │ │ └── index.ts │ ├── SectionBlock │ │ ├── SectionBlock.module.scss │ │ ├── SectionBlock.stories.tsx │ │ ├── SectionBlock.tsx │ │ └── index.ts │ ├── SectionTitle │ │ ├── SectionTItle.stories.tsx │ │ ├── SectionTitle.module.scss │ │ ├── SectionTitle.tsx │ │ └── index.ts │ ├── Select │ │ ├── Select.module.scss │ │ ├── Select.stories.tsx │ │ ├── Select.tsx │ │ └── index.ts │ ├── Tabs │ │ ├── Tabs.module.scss │ │ ├── Tabs.stories.tsx │ │ ├── Tabs.tsx │ │ ├── TabsTab.tsx │ │ └── index.ts │ └── Textarea │ │ ├── Textarea.module.scss │ │ ├── Textarea.stories.tsx │ │ ├── Textarea.tsx │ │ └── index.ts ├── constants │ ├── ButtonTypes.ts │ ├── ControlSizes.ts │ └── index.ts ├── index.tsx └── scss │ ├── resources │ ├── _colors.scss │ └── _fonts.scss │ └── style │ ├── _base.scss │ ├── _fonts.scss │ └── style.scss ├── storybook-static └── favicon.ico ├── tsconfig.json ├── typings ├── react-portal.d.ts └── scss.d.ts ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "airbnb-typescript" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.json" 8 | } 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | build-storybook.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | constants/ 3 | **/*.html -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack.config'); 2 | 3 | module.exports = { 4 | stories: [ 5 | '../src/**/*.stories.mdx', 6 | '../src/**/*.stories.@(js|jsx|ts|tsx)' 7 | ], 8 | addons: [ 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | '@storybook/addon-interactions' 12 | ], 13 | framework: '@storybook/react', 14 | core: { 15 | builder: '@storybook/builder-webpack5' 16 | }, 17 | webpackFinal: async (config) => { 18 | config.module.rules.push(webpackConfig.module.rules[1]); 19 | config.plugins.push(webpackConfig.plugins[1]); 20 | return config; 21 | }, 22 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/scss/style/style.scss'; 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.7 4 | * Support `icon` in `OptionMenu` 5 | 6 | ## v0.2.6 7 | * Updated `OptionMenu` with `floating-ui` 8 | 9 | ## v0.2.5 10 | * Added custom `ref` to `Tabs` component to programmatically update local state 11 | * Added `hidden` option to tabs 12 | 13 | ## v0.2.4 14 | * Added `extra` prop to add extra content to the side of the tabs list 15 | 16 | ## v0.2.3 17 | * Fix `trigger` required in `OptionMenu` props 18 | 19 | ## v0.2.2 20 | * Add `placement` option to `OptionMenu` 21 | * Added option to custom render `OptionMenu` trigger 22 | * Added `label` option for `IconButton` 23 | 24 | ## v0.2.1 25 | * Minor fix to `Button` props typing 26 | 27 | ## v0.2.0 28 | * Updated components for `react@>=17` 29 | * Removed `devServer` and replaced it with storybook 30 | ## v0.1.5 31 | * Added `onTabClick` property to `Tabs` component 32 | 33 | ## v0.1.5 34 | * Added `tabClassName` property to `Tabs` component 35 | 36 | ## v0.1.0 37 | * Added `portal` functionality for `OptionMenu` and `Select` 38 | * Added prop `optionListClassName` to both `OptionMenu` and `Select` 39 | * Fixed some `React` warnings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma React UI Kit 2 | [![npm-badge](https://badge.fury.io/js/figma-react-ui-kit.svg)](https://www.npmjs.com/package/figma-react-ui-kit) 3 | [Latest Storybook](https://development--6276bd991b0b1d004aff360e.chromatic.com) 4 | This library contains some generic components for creating Figma styled UI's 5 | 6 | ## Getting started 7 | Install the libray 8 | ``` 9 | npm install --save figma-react-ui-kit 10 | ``` 11 | 12 | ## Import css 13 | Be sure to import the css files. 14 | ``` 15 | node_modules/figma-react-ui-kit/lib/index.css 16 | node_modules/figma-react-ui-kit/lib/style.css 17 | ``` 18 | 19 | ## Included components 20 | ### Button 21 | ```html 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | ### IconButton 29 | ```html 30 | 31 | 32 | 33 | ``` 34 | 35 | ### Input 36 | ```html 37 | Some label 38 | 39 | 40 | 41 | ``` 42 | 43 | ### Textarea 44 | ```html 45 | 51 | ``` 52 | 53 | ### Select 54 | ```jsx 55 | 34 | {label} 35 | 36 | ) 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | -------------------------------------------------------------------------------- /src/components/IconButton/IconButton.module.scss: -------------------------------------------------------------------------------- 1 | .iconButton { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-grow: 0; 6 | box-sizing: border-box; 7 | cursor: pointer; 8 | height: 3.2em; 9 | padding: 0 1em; 10 | border: 1px solid transparent; 11 | border-radius: 3px; 12 | color: $color-darkgray; 13 | font-family: $font-family-body; 14 | background-color: transparent; 15 | 16 | &.extraRound { 17 | border-radius: 6px; 18 | } 19 | 20 | &::-moz-focus-inner { 21 | border: 0; 22 | } 23 | 24 | &:hover { 25 | background-color: rgba($color-dark, .06); 26 | } 27 | 28 | &.on, 29 | &:active { 30 | border-color: $color-primary; 31 | box-shadow: inset 0 0 0 1px $color-primary; 32 | } 33 | 34 | &.on { 35 | background-color: $color-primary; 36 | color: $color-light; 37 | } 38 | 39 | &.muted { 40 | color: $color-mediumgray; 41 | 42 | &:hover { 43 | color: $color-dark; 44 | } 45 | } 46 | 47 | &.s { 48 | font-size: $font-size-xs; 49 | } 50 | 51 | &.m { 52 | font-size: $font-size-m; 53 | } 54 | 55 | &.l { 56 | font-size: $font-size-l; 57 | } 58 | 59 | svg { 60 | display: block; 61 | line-height: 3.2em; 62 | height: 1em; 63 | width: auto; 64 | fill: currentColor; 65 | } 66 | 67 | .content { 68 | display: flex; 69 | align-items: center; 70 | } 71 | 72 | .label { 73 | margin-left: 8px; 74 | } 75 | } -------------------------------------------------------------------------------- /src/components/IconButton/IconButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { IconButton } from './IconButton'; 4 | import { ControlSizes } from '../../constants'; 5 | 6 | export default { 7 | title: 'IconButton', 8 | component: IconButton, 9 | argTypes: { 10 | }, 11 | } as ComponentMeta; 12 | 13 | const Template: ComponentStory = (args) => ; 14 | 15 | export const Standard = Template.bind({}); 16 | Standard.args = { 17 | buttonSize: ControlSizes.S, 18 | extraRound: false, 19 | on: false, 20 | label: 'Click me', 21 | children: ( 22 | 23 | 24 | 25 | ), 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './IconButton.module.scss'; 3 | import cx from 'classnames'; 4 | import { ControlSizes } from '../../constants'; 5 | 6 | export type IconButtonProps = React.ButtonHTMLAttributes & { 7 | buttonSize?: ControlSizes; 8 | on?: boolean; 9 | extraRound?: boolean; 10 | muted?: boolean; 11 | label?: React.ReactNode; 12 | } 13 | 14 | export const IconButton = React.forwardRef(({ 15 | buttonSize = ControlSizes.S, 16 | on = false, 17 | extraRound = false, 18 | muted = false, 19 | label, 20 | className, 21 | children, 22 | ...props 23 | }, ref) => { 24 | return ( 25 | 44 | ) 45 | }); -------------------------------------------------------------------------------- /src/components/IconButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IconButton'; 2 | -------------------------------------------------------------------------------- /src/components/Input/Input.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | display: inline-block; 3 | position: relative; 4 | flex-grow: 1; 5 | } 6 | 7 | .inlineLabel { 8 | position: absolute; 9 | top: .82em; 10 | left: 0.857em; 11 | font-size: $font-size-s; 12 | font-family: $font-family-body; 13 | color: $color-mediumgray; 14 | 15 | &.s { 16 | font-size: $font-size-s; 17 | } 18 | 19 | &.m { 20 | font-size: $font-size-m; 21 | } 22 | 23 | &.l { 24 | font-size: $font-size-l; 25 | } 26 | 27 | svg { 28 | display: block; 29 | margin-top: .2em; 30 | height: .8em; 31 | width: auto; 32 | } 33 | 34 | path { 35 | fill: currentColor; 36 | } 37 | } 38 | 39 | .input { 40 | display: block; 41 | width: 100%; 42 | box-sizing: border-box; 43 | padding: 0 0.857em; 44 | border: 0; 45 | outline: 0; 46 | font-family: $font-family-body; 47 | font-weight: 500; 48 | height: 2.85em; 49 | color: $color-darkgray; 50 | border-radius: 3px; 51 | box-shadow: inset 0 0 0 1px $color-gray; 52 | 53 | &.extraRound { 54 | border-radius: 6px; 55 | } 56 | 57 | &::placeholder { 58 | color: $color-mediumgray; 59 | } 60 | 61 | &.cleanBorder { 62 | box-shadow: none; 63 | 64 | &:hover { 65 | box-shadow: inset 0 0 0 1px $color-gray; 66 | } 67 | } 68 | 69 | &.cleanBorder:focus, 70 | &:focus { 71 | box-shadow: inset 0 0 0 2px $color-primary; 72 | } 73 | 74 | &:focus+.inlineLabel { 75 | color: $color-primary; 76 | } 77 | 78 | &.s { 79 | font-size: $font-size-s; 80 | } 81 | 82 | &.m { 83 | font-size: $font-size-m; 84 | } 85 | 86 | &.l { 87 | font-size: $font-size-l; 88 | } 89 | } -------------------------------------------------------------------------------- /src/components/Input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { Input } from './Input'; 4 | import { ControlSizes } from '../../constants'; 5 | 6 | export default { 7 | title: 'Input', 8 | component: Input, 9 | argTypes: { 10 | }, 11 | } as ComponentMeta; 12 | 13 | const Template: ComponentStory = (args) => ; 14 | 15 | export const Standard = Template.bind({}); 16 | Standard.args = { 17 | inputSize: ControlSizes.S, 18 | inlineLabel: 'Label', 19 | cleanBorder: false, 20 | extraRound: false, 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Input.module.scss'; 3 | import cx from 'classnames'; 4 | import { ControlSizes } from '../../constants'; 5 | 6 | export type InputProps = React.InputHTMLAttributes & { 7 | inputSize?: ControlSizes; 8 | inlineLabel?: string | React.ReactNode; 9 | cleanBorder?: boolean; 10 | extraRound?: boolean; 11 | } 12 | 13 | export const Input = React.forwardRef(({ 14 | inlineLabel = '', 15 | cleanBorder = false, 16 | extraRound = false, 17 | inputSize = ControlSizes.S, 18 | style, 19 | className, 20 | children, 21 | ...props 22 | }, ref) => { 23 | const [inlineLabelSize, setInlineLabelSize] = React.useState(0); 24 | 25 | const handleInlineLabelRef = React.useCallback((el?: HTMLSpanElement | null) => { 26 | if (el) { 27 | setInlineLabelSize(el.offsetWidth); 28 | } 29 | }, []); 30 | 31 | return ( 32 | 56 | ); 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Input'; 2 | -------------------------------------------------------------------------------- /src/components/InputLabel/InputLabel.module.scss: -------------------------------------------------------------------------------- 1 | .inputLabel { 2 | display: block; 3 | margin: 0 0 4px 0; 4 | font-size: $font-size-s; 5 | font-family: $font-family-body; 6 | font-weight: 500; 7 | color: $color-mediumgray; 8 | } -------------------------------------------------------------------------------- /src/components/InputLabel/InputLabel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { InputLabel } from './InputLabel'; 4 | 5 | export default { 6 | title: 'InputLabel', 7 | component: InputLabel, 8 | argTypes: { 9 | }, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Standard = Template.bind({}); 15 | Standard.args = { 16 | children: 'Label', 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/InputLabel/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './InputLabel.module.scss'; 3 | import cx from 'classnames'; 4 | 5 | export type InputLabelProps = React.LabelHTMLAttributes; 6 | 7 | export const InputLabel = React.forwardRef(({ 8 | className, 9 | children, 10 | ...props 11 | }, ref) => { 12 | return ( 13 | 23 | ) 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/InputLabel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InputLabel'; 2 | -------------------------------------------------------------------------------- /src/components/OptionMenu/OptionMenu.module.scss: -------------------------------------------------------------------------------- 1 | .optionMenu { 2 | position: relative; 3 | display: inline-block; 4 | 5 | &.extraRound { 6 | .optionList { 7 | border-radius: 6px; 8 | } 9 | } 10 | } 11 | 12 | .optionList { 13 | visibility: hidden; 14 | pointer-events: none; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | white-space: nowrap; 19 | padding: 0; 20 | margin: 0; 21 | padding: .4em 0; 22 | list-style: none; 23 | background-color: $color-darkgray; 24 | color: $color-light; 25 | font-family: $font-family-body; 26 | user-select: none; 27 | border-radius: 3px; 28 | box-shadow: 0 0 5px rgba($color-dark, .1); 29 | 30 | &.isOpen { 31 | visibility: visible; 32 | pointer-events: all; 33 | } 34 | 35 | &.hangLeft { 36 | left: auto; 37 | right: 0; 38 | } 39 | 40 | &.s { 41 | font-size: $font-size-s; 42 | } 43 | 44 | &.m { 45 | font-size: $font-size-m; 46 | } 47 | 48 | &.l { 49 | font-size: $font-size-l; 50 | } 51 | } 52 | 53 | .option { 54 | cursor: pointer; 55 | padding: .5em 1em .45em 1em; 56 | 57 | &:hover { 58 | background: $color-primary; 59 | } 60 | 61 | .content { 62 | display: flex; 63 | align-items: center; 64 | 65 | .label { 66 | margin-left: 8px; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/components/OptionMenu/OptionMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { OptionMenu } from './OptionMenu'; 4 | import { ControlSizes } from '../../constants'; 5 | 6 | export default { 7 | title: 'OptionMenu', 8 | component: OptionMenu, 9 | argTypes: { 10 | }, 11 | } as ComponentMeta; 12 | 13 | const Template: ComponentStory = (args) => ; 14 | 15 | const Icon = () => ( 16 | 17 | 18 | 19 | ) 20 | 21 | export const Standard = Template.bind({}); 22 | Standard.args = { 23 | optionMenuSize: ControlSizes.S, 24 | options: [ 25 | { 26 | icon: Icon, 27 | label: 'Option 1', 28 | value: 'option-1', 29 | onClick: console.log, 30 | }, 31 | { 32 | icon: Icon, 33 | label: 'Option 2', 34 | value: 'option-2', 35 | onClick: console.log, 36 | }, 37 | ], 38 | children: ( 39 |
40 | 41 | 42 | 43 | More 44 |
45 | ) 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/OptionMenu/OptionMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './OptionMenu.module.scss'; 3 | import cx from 'classnames'; 4 | import { IconButton } from '../IconButton'; 5 | import { ControlSizes } from '../../constants'; 6 | import { Portal } from 'react-portal'; 7 | import { useFloating, shift, autoUpdate, offset, Placement } from '@floating-ui/react-dom'; 8 | 9 | export interface IOption { 10 | label: string; 11 | value: V; 12 | icon?: React.ComponentType; 13 | onClick?: (val: V) => void; 14 | } 15 | 16 | export type TriggerProps = { 17 | on?: boolean; 18 | extraRound?: boolean; 19 | buttonSize?: ControlSizes; 20 | onClick?: React.MouseEventHandler; 21 | } 22 | 23 | export type OptionMenuProps = React.HTMLAttributes & { 24 | open?: boolean; 25 | optionMenuSize?: ControlSizes; 26 | placement?: Placement; 27 | overlayTrigger?: boolean; 28 | stopPropagation?: boolean; 29 | extraRound?: boolean; 30 | options: IOption[]; 31 | optionListClassName?: string; 32 | trigger?: (props: TriggerProps) => React.ReactNode; 33 | onOpen?: () => void; 34 | onClose?: () => void; 35 | } 36 | 37 | export const OptionMenu = React.forwardRef(({ 38 | optionMenuSize = ControlSizes.S, 39 | stopPropagation = false, 40 | overlayTrigger = true, 41 | open, 42 | options, 43 | extraRound, 44 | className, 45 | optionListClassName, 46 | placement, 47 | trigger, 48 | onOpen, 49 | onClose, 50 | children, 51 | ...props 52 | }, ref) => { 53 | const [isOpen, setIsOpen] = React.useState(false); 54 | const {x, y, strategy, reference, floating, refs} = useFloating({ 55 | placement: placement || 'bottom-start', 56 | middleware: overlayTrigger ? [shift()] : [shift(), offset(4)], 57 | whileElementsMounted: autoUpdate, 58 | }); 59 | 60 | React.useImperativeHandle(ref, () => ( 61 | refs.reference.current as (HTMLDivElement | null) 62 | )); 63 | 64 | const isOpenValue = React.useMemo(() => { 65 | if (typeof open === 'boolean') return open; 66 | return isOpen; 67 | }, [open, isOpen]); 68 | 69 | const handleWindowClick = React.useCallback((event: Event) => { 70 | if ( 71 | isOpenValue 72 | && refs.floating.current 73 | && (!(event.target instanceof Node) || !refs.floating.current.contains(event.target as any)) 74 | ) { 75 | setIsOpen(false); 76 | if (onClose) onClose(); 77 | } 78 | }, [isOpenValue, onClose]); 79 | 80 | const handleClick = React.useCallback((event: React.MouseEvent) => { 81 | if (stopPropagation) { 82 | event.stopPropagation(); 83 | window.dispatchEvent(new Event('click')); 84 | } 85 | setIsOpen(!isOpenValue); 86 | if (isOpenValue && onClose) onClose(); 87 | else if (!isOpenValue && onOpen) onOpen(); 88 | }, [stopPropagation, isOpenValue, onOpen, onClose]); 89 | 90 | const handleOptionClick = React.useCallback((opt: IOption) => { 91 | setIsOpen(false); 92 | if (opt.onClick) opt.onClick(opt.value); 93 | if (onClose) onClose(); 94 | }, [onClose]); 95 | 96 | const triggerElement = React.useMemo(() => { 97 | if (trigger) { 98 | return trigger({ 99 | on: isOpenValue, 100 | extraRound: extraRound, 101 | buttonSize: optionMenuSize, 102 | onClick: handleClick, 103 | }) 104 | } 105 | 106 | return ( 107 | 113 | 114 | 115 | 116 | 117 | ) 118 | }, [trigger, isOpenValue, extraRound, optionMenuSize, handleClick]); 119 | 120 | const optionsList = React.useMemo(() => { 121 | return ( 122 |
    136 | {options.map(({ icon: Icon, ...opt }) => ( 137 |
  • ) => { 141 | if (stopPropagation) { 142 | event.stopPropagation(); 143 | } 144 | handleOptionClick(opt); 145 | }} 146 | > 147 |
    148 | {!!Icon && } 149 | {opt.label} 150 |
    151 |
  • 152 | ))} 153 |
154 | ); 155 | }, [x, y, strategy, floating, isOpenValue, placement, stopPropagation, options, optionMenuSize, optionListClassName, handleOptionClick]); 156 | 157 | React.useEffect(() => { 158 | if (isOpenValue) { 159 | window.requestAnimationFrame(() => ( 160 | window.addEventListener('click', handleWindowClick) 161 | )); 162 | } 163 | return () => { 164 | window.requestAnimationFrame(() => { 165 | window.removeEventListener('click', handleWindowClick); 166 | }); 167 | }; 168 | }, [isOpenValue, handleWindowClick]); 169 | 170 | 171 | return ( 172 |
181 | {overlayTrigger &&
} 182 | {triggerElement} 183 | {isOpenValue && ( 184 | 185 | {optionsList} 186 | 187 | )} 188 |
189 | ); 190 | }) as ( 191 | props: OptionMenuProps & { ref?: React.ForwardedRef } 192 | ) => React.ReactElement | null; 193 | -------------------------------------------------------------------------------- /src/components/OptionMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OptionMenu'; 2 | -------------------------------------------------------------------------------- /src/components/Section/Section.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | padding: 8px; 3 | border-top: 1px solid $color-gray; 4 | 5 | &:first-child { 6 | border-top: 0; 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/Section/Section.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { Section } from './Section'; 4 | 5 | export default { 6 | title: 'Section', 7 | component: Section, 8 | argTypes: { 9 | }, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ( 13 | <> 14 |
15 |
16 |
17 |
18 | 19 | ); 20 | 21 | export const Standard = Template.bind({}); 22 | Standard.args = { 23 | children: 'Hello world', 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Section.module.scss'; 3 | import cx from 'classnames'; 4 | 5 | export type SectionProps = React.HTMLAttributes; 6 | 7 | export const Section = React.forwardRef(({ 8 | children, 9 | className, 10 | ...props 11 | }, ref) => { 12 | return ( 13 |
21 | {children} 22 |
23 | ) 24 | }) -------------------------------------------------------------------------------- /src/components/Section/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Section'; 2 | -------------------------------------------------------------------------------- /src/components/SectionBlock/SectionBlock.module.scss: -------------------------------------------------------------------------------- 1 | .sectionBlock { 2 | padding: 8px; 3 | } -------------------------------------------------------------------------------- /src/components/SectionBlock/SectionBlock.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { SectionBlock } from './SectionBlock'; 4 | 5 | export default { 6 | title: 'SectionBlock', 7 | component: SectionBlock, 8 | argTypes: { 9 | }, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Standard = Template.bind({}); 15 | Standard.args = { 16 | children: 'Hello world', 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/SectionBlock/SectionBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './SectionBlock.module.scss'; 3 | import cx from 'classnames'; 4 | 5 | export type SectionBlockProps = React.HTMLAttributes; 6 | 7 | export const SectionBlock = React.forwardRef(({ 8 | children, 9 | className, 10 | ...props 11 | }, ref) => { 12 | return ( 13 |
21 | {children} 22 |
23 | ) 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/SectionBlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SectionBlock'; 2 | -------------------------------------------------------------------------------- /src/components/SectionTitle/SectionTItle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { SectionTitle } from './SectionTitle'; 4 | 5 | export default { 6 | title: 'SectionTitle', 7 | component: SectionTitle, 8 | argTypes: { 9 | }, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Standard = Template.bind({}); 15 | Standard.args = { 16 | children: 'Hello world', 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/SectionTitle/SectionTitle.module.scss: -------------------------------------------------------------------------------- 1 | .sectionTitle { 2 | margin: 0; 3 | padding: 8px; 4 | font-family: $font-family-body; 5 | font-size: $font-size-s; 6 | font-weight: 500; 7 | } -------------------------------------------------------------------------------- /src/components/SectionTitle/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './SectionTitle.module.scss'; 3 | import cx from 'classnames'; 4 | 5 | export type SectionTitleProps = React.HTMLAttributes; 6 | 7 | export const SectionTitle = React.forwardRef(({ 8 | children, 9 | className, 10 | ...props 11 | }, ref) => { 12 | return ( 13 |

21 | {children} 22 |

23 | ); 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/SectionTitle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SectionTitle'; 2 | -------------------------------------------------------------------------------- /src/components/Select/Select.module.scss: -------------------------------------------------------------------------------- 1 | .select, 2 | .optionsList { 3 | &.s { 4 | font-size: $font-size-s; 5 | } 6 | 7 | &.m { 8 | font-size: $font-size-m; 9 | } 10 | 11 | &.l { 12 | font-size: $font-size-l; 13 | } 14 | } 15 | 16 | .select { 17 | position: relative; 18 | box-sizing: border-box; 19 | display: flex; 20 | justify-content: flex-start; 21 | align-items: center; 22 | cursor: pointer; 23 | border: 1px solid rgba($color-dark, .1); 24 | height: 2.85em; 25 | padding: 0 0.857em; 26 | color: $color-darkgray; 27 | border-radius: 3px; 28 | user-select: none; 29 | 30 | &.extraRound { 31 | 32 | &, 33 | .optionsList { 34 | border-radius: 6px; 35 | } 36 | } 37 | 38 | &.cleanBorder { 39 | border: 1px solid transparent; 40 | 41 | &:hover { 42 | border: 1px solid rgba($color-dark, .1); 43 | 44 | .arrow { 45 | fill: $color-darkgray; 46 | } 47 | } 48 | 49 | .arrow { 50 | fill: $color-mediumgray; 51 | } 52 | } 53 | 54 | &.focus, 55 | &.cleanBorder.focus, 56 | &:focus { 57 | border: 2px solid $color-primary; 58 | padding: 0 calc(0.857em - 1px); 59 | } 60 | 61 | .input { 62 | display: none; 63 | } 64 | 65 | .placeholder { 66 | color: $color-mediumgray; 67 | } 68 | 69 | .value { 70 | display: flex; 71 | justify-content: flex-start; 72 | align-items: center; 73 | font-family: $font-family-body; 74 | 75 | .icon { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | margin-right: .5em; 80 | 81 | svg { 82 | height: 1em; 83 | width: auto; 84 | fill: currentColor; 85 | } 86 | } 87 | } 88 | 89 | .arrow { 90 | display: block; 91 | margin-left: auto; 92 | fill: $color-darkgray; 93 | } 94 | } 95 | 96 | .optionsList { 97 | position: absolute; 98 | display: block; 99 | overflow: auto; 100 | top: -2px; 101 | left: -2px; 102 | right: -2px; 103 | padding: .4em 0; 104 | margin: 0; 105 | list-style: none; 106 | background: $color-darkgray; 107 | color: $color-light; 108 | border-radius: 3px; 109 | box-shadow: 0 0 5px rgba($color-dark, .1); 110 | 111 | &.portaled { 112 | right: auto; 113 | } 114 | 115 | .option { 116 | position: relative; 117 | display: block; 118 | padding: .5em 1em .45em 3em; 119 | font-size: inherit; 120 | font-family: $font-family-body; 121 | 122 | &:hover { 123 | background: $color-primary; 124 | } 125 | 126 | .checkmark { 127 | position: absolute; 128 | top: 50%; 129 | left: .5em; 130 | fill: $color-light; 131 | transform: translateY(-48%); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/components/Select/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { Select } from './Select'; 4 | 5 | export default { 6 | title: 'Select', 7 | component: Select, 8 | argTypes: { 9 | }, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/Textarea/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Textarea'; 2 | -------------------------------------------------------------------------------- /src/constants/ButtonTypes.ts: -------------------------------------------------------------------------------- 1 | export enum ButtonTypes { 2 | PRIMARY = 'primary', 3 | GHOST = 'ghost', 4 | DESTRUCTIVE = 'destructive', 5 | } 6 | -------------------------------------------------------------------------------- /src/constants/ControlSizes.ts: -------------------------------------------------------------------------------- 1 | export enum ControlSizes { 2 | S = 's', 3 | M = 'm' 4 | }; 5 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ControlSizes'; 2 | export * from './ButtonTypes'; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './components/Button'; 3 | export * from './components/IconButton'; 4 | export * from './components/Input'; 5 | export * from './components/InputLabel'; 6 | export * from './components/Textarea'; 7 | export * from './components/Checkbox'; 8 | export * from './components/Section'; 9 | export * from './components/SectionBlock'; 10 | export * from './components/SectionTitle'; 11 | export * from './components/Select'; 12 | export * from './components/OptionMenu'; 13 | export * from './components/Tabs'; -------------------------------------------------------------------------------- /src/scss/resources/_colors.scss: -------------------------------------------------------------------------------- 1 | $color-light: white; 2 | $color-dark: black; 3 | $color-gray: #e5e5e5; 4 | $color-mediumgray: #b3b3b3; 5 | $color-darkgray: #333; 6 | $color-primary: #18a0fb; 7 | $color-secondary: #EF492D; -------------------------------------------------------------------------------- /src/scss/resources/_fonts.scss: -------------------------------------------------------------------------------- 1 | $font-family-body: Inter, Helvetica, Arial, sans-serif; 2 | 3 | $font-size-xs: 10px; 4 | $font-size-s: 11px; 5 | $font-size-m: 12px; 6 | $font-size-l: 14px; 7 | $font-size-xl: 18px; -------------------------------------------------------------------------------- /src/scss/style/_base.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /src/scss/style/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url("https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.7") format("woff2"), url("https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.7") format("woff"); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Inter'; 10 | font-style: normal; 11 | font-weight: 500; 12 | src: url("https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.7") format("woff2"), url("https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.7") format("woff"); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Inter'; 17 | font-style: normal; 18 | font-weight: 600; 19 | src: url("https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.7") format("woff2"), url("https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.7") format("woff"); 20 | } -------------------------------------------------------------------------------- /src/scss/style/style.scss: -------------------------------------------------------------------------------- 1 | @import "./fonts"; 2 | @import "./base"; -------------------------------------------------------------------------------- /storybook-static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiamMartens/figma-react-ui-kit/eaa37af6b09da96cb45135a50be04647b883f2ad/storybook-static/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "lib": [ 6 | "es2017", 7 | "es2015", 8 | "dom" 9 | ], 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "strictNullChecks": true, 14 | "outDir": "./lib", 15 | "declaration": true 16 | }, 17 | "include": [ 18 | "./src/**/*.ts", 19 | "./src/**/*.tsx", 20 | "./typings/*.ts" 21 | ] 22 | } -------------------------------------------------------------------------------- /typings/react-portal.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-portal' { 2 | export const Portal: React.ComponentType<{ 3 | node?: HTMLElement; 4 | }>; 5 | } -------------------------------------------------------------------------------- /typings/scss.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: {[className: string]: string}; 3 | export = content; 4 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const FixStyleOnlyEntriesPlugin = require("webpack-fix-style-only-entries"); 5 | 6 | const isProduction = process.env.NODE_ENV === 'production'; 7 | 8 | module.exports = { 9 | entry: { 10 | style: path.resolve(__dirname, 'src/scss/style/style.scss'), 11 | index: path.resolve(__dirname, 'src/index.tsx'), 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'lib'), 15 | filename: '[name].js', 16 | ...(isProduction && ({ 17 | library: 'FigmaUIKit', 18 | libraryTarget: 'umd' 19 | })) 20 | }, 21 | ...(isProduction && ({ 22 | externals: ['react', 'react-dom', 'react-portal'], 23 | })), 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | loader: 'ts-loader', 29 | exclude: /node_modules/ 30 | }, 31 | { 32 | test: /\.scss$/, 33 | use: [ 34 | MiniCssExtractPlugin.loader, 35 | { 36 | loader: 'css-loader', 37 | options: { 38 | modules: { 39 | localIdentName: '[name]__[local]___[hash:base64:5]', 40 | exportLocalsConvention: 'camelCase', 41 | }, 42 | sourceMap: false, 43 | } 44 | }, 45 | { 46 | loader: 'sass-loader', 47 | options: { 48 | sourceMap: false, 49 | } 50 | }, 51 | { 52 | loader: 'sass-resources-loader', 53 | options: { 54 | resources: fs.readdirSync(path.join(__dirname, 'src/scss/resources')).map(file => ( 55 | path.join(__dirname, 'src/scss/resources', file) 56 | )) 57 | } 58 | } 59 | ] 60 | } 61 | ] 62 | }, 63 | resolve: { 64 | extensions: ['.tsx', '.ts', '.js', '.scss'] 65 | }, 66 | plugins: [ 67 | new FixStyleOnlyEntriesPlugin(), 68 | new MiniCssExtractPlugin({ 69 | filename: '[name].css', 70 | chunkFilename: '[id].css' 71 | }), 72 | ], 73 | }; --------------------------------------------------------------------------------