├── __mocks__ └── fileMock.js ├── src ├── utils │ ├── index.ts │ ├── option.ts │ ├── withOptions.ts │ ├── styled.ts │ └── stateful.tsx ├── components │ ├── Switcher │ │ ├── components │ │ │ ├── index.ts │ │ │ └── Item.tsx │ │ ├── index.m.scss │ │ ├── styled.ts │ │ └── index.tsx │ ├── Button │ │ ├── sheen.png │ │ ├── index.tsx │ │ └── index.scss │ ├── RadioInput │ │ ├── index.scss │ │ └── index.tsx │ ├── Frame │ │ ├── sub-border-primary-horizontal.png │ │ ├── sub-border-secondary-horizontal.png │ │ ├── index.tsx │ │ └── index.scss │ ├── RuneBuilder │ │ ├── Rune │ │ │ ├── Body │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── Slot │ │ │ │ ├── slots.ts │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Ring │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── gradients.tsx │ ├── ButtonGroup │ │ ├── index.tsx │ │ └── index.scss │ ├── Icons │ │ ├── styled.ts │ │ └── index.tsx │ ├── Dropdown │ │ ├── assets │ │ │ └── check.svg │ │ ├── components │ │ │ ├── Menu.tsx │ │ │ ├── Arrow.tsx │ │ │ ├── Wrapper.tsx │ │ │ └── Option.tsx │ │ ├── index.m.scss │ │ ├── keyboardEvents.ts │ │ └── index.tsx │ ├── TextInput │ │ ├── close.svg │ │ ├── index.tsx │ │ └── index.scss │ ├── Badge │ │ ├── index.tsx │ │ └── styled.ts │ ├── Card │ │ ├── index.tsx │ │ └── styled.ts │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── RadioInput.js.snap │ │ │ └── Checkbox.js.snap │ │ ├── RadioOption.jsx │ │ ├── Checkbox.jsx │ │ └── RadioInput.jsx │ ├── RadioOption │ │ ├── index.tsx │ │ └── index.scss │ ├── Checkbox │ │ ├── index.tsx │ │ └── index.scss │ └── SliderInput │ │ ├── index.scss │ │ ├── Handle.tsx │ │ └── index.tsx ├── assets │ ├── card-flare.png │ ├── icons │ │ ├── comic.png │ │ ├── media.png │ │ └── story.png │ ├── search-icon.png │ ├── card-overlay.png │ └── button-bg-pattern.png ├── global.d.ts ├── css │ ├── _mixins.scss │ ├── input.scss │ ├── global.scss │ └── button.scss ├── variables.scss ├── theme.js └── index.ts ├── .prettierrc ├── .storybook ├── public │ ├── lcu-bg.png │ └── universe-bg.jpg ├── manager-head.html ├── addons.js ├── webpack.config.js ├── config.js └── index.css ├── .gitattributes ├── .travis.yml ├── setup-tests.js ├── stories ├── Card.js ├── Badge.js ├── Todo.js ├── Switcher.js ├── RadioInput.js ├── Checkbox.jsx ├── Button.js ├── TextInput.js ├── Frame.js ├── SliderInput.js └── Dropdown.js ├── .editorconfig ├── README.md ├── .babelrc.js ├── .gitignore ├── jest.json ├── tsconfig.json ├── LICENSE ├── tslint.json ├── package.json └── webpack.config.js /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './styled'; 2 | export { withOptions } from './withOptions'; 3 | -------------------------------------------------------------------------------- /src/components/Switcher/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Item } from './Item'; 2 | 3 | export { Item }; 4 | -------------------------------------------------------------------------------- /src/assets/card-flare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/card-flare.png -------------------------------------------------------------------------------- /src/assets/icons/comic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/icons/comic.png -------------------------------------------------------------------------------- /src/assets/icons/media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/icons/media.png -------------------------------------------------------------------------------- /src/assets/icons/story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/icons/story.png -------------------------------------------------------------------------------- /src/assets/search-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/search-icon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /.storybook/public/lcu-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/.storybook/public/lcu-bg.png -------------------------------------------------------------------------------- /src/assets/card-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/card-overlay.png -------------------------------------------------------------------------------- /src/components/Button/sheen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/components/Button/sheen.png -------------------------------------------------------------------------------- /.storybook/public/universe-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/.storybook/public/universe-bg.jpg -------------------------------------------------------------------------------- /src/assets/button-bg-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/assets/button-bg-pattern.png -------------------------------------------------------------------------------- /src/utils/option.ts: -------------------------------------------------------------------------------- 1 | export interface Option { 2 | label?: string; 3 | value: T; 4 | [key: string]: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.m.scss' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.ico binary 5 | *.icns binary 6 | *.otf binary 7 | *.ogg binary 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | dist: trusty 4 | node_js: 5 | - 7 6 | script: 7 | - yarn lint 8 | - yarn test 9 | -------------------------------------------------------------------------------- /src/components/RadioInput/index.scss: -------------------------------------------------------------------------------- 1 | .radioInput { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | outline: none; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Frame/sub-border-primary-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/components/Frame/sub-border-primary-horizontal.png -------------------------------------------------------------------------------- /src/components/Frame/sub-border-secondary-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-coimbra21/react-hextech/HEAD/src/components/Frame/sub-border-secondary-horizontal.png -------------------------------------------------------------------------------- /setup-tests.js: -------------------------------------------------------------------------------- 1 | import 'jest-enzyme'; 2 | import { configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/Body/index.scss: -------------------------------------------------------------------------------- 1 | .quintBody { 2 | fill: #182632; 3 | } 4 | 5 | .quintBodyStroke { 6 | fill: url(#quintBodyStroke); 7 | } 8 | 9 | .quintBodyInner { 10 | fill: url(#quintBodyInner); 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addons'; 2 | 3 | import '@storybook/addon-options/register'; 4 | import '@storybook/addon-actions/register'; 5 | import '@storybook/addon-a11y/register'; 6 | import '@storybook/addon-backgrounds/register'; 7 | -------------------------------------------------------------------------------- /stories/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Card } from '../src'; 6 | 7 | storiesOf('Card', module).add('with text', () => ( 8 | 9 | )); 10 | -------------------------------------------------------------------------------- /stories/Badge.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Badge } from '../src'; 6 | 7 | storiesOf('Badge', module).add('with text', () => ( 8 | 9 | )); 10 | -------------------------------------------------------------------------------- /stories/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { RuneBuilder } from '../src'; 4 | 5 | storiesOf('TODO', module) 6 | .add('Mastery Builder', () => null) 7 | .add('Rune Builder', () => ) 8 | .add('Champion Squares w/ Mastery', () => null); 9 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/Slot/slots.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 27: { 3 | x: 60.171, 4 | y: 104.951, 5 | type: 'quintessence', 6 | }, 7 | 28: { 8 | x: 417.671, 9 | y: 104.951, 10 | type: 'quintessence', 11 | }, 12 | 29: { 13 | x: 238.157, 14 | y: 414.789, 15 | type: 'quintessence', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Ring/index.scss: -------------------------------------------------------------------------------- 1 | .slotOuter { 2 | fill: #010A13; 3 | fill-opacity: 0.5; 4 | } 5 | 6 | .slotInner { 7 | fill: none; 8 | stroke: #362D1B; 9 | stroke-miterlimit: 10; 10 | } 11 | 12 | .slotMiddle { 13 | fill: none; 14 | stroke: #6B5024; 15 | stroke-width: 3; 16 | stroke-miterlimit: 10; 17 | } 18 | 19 | .st3 { 20 | fill: none; 21 | stroke: #6B5024; 22 | stroke-miterlimit: 10; 23 | } 24 | -------------------------------------------------------------------------------- /src/css/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin button { 2 | display: inline-block; 3 | position: relative; 4 | font-family: 'Beaufort', 'Arial'; 5 | text-transform: uppercase; 6 | outline: none; 7 | color: $gold-medium; 8 | font-weight: bold; 9 | letter-spacing: 1px; 10 | white-space: nowrap; 11 | padding: 9px 21px; 12 | cursor: pointer; 13 | line-height: 1; 14 | box-shadow: 0 0 1px 1px #010a13, inset 0 0 1px 1px #010a13; 15 | background: #1e2328; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import Slot from './Slot'; 4 | import Body from './Body'; 5 | 6 | interface RuneProps { 7 | slot: any; 8 | } 9 | 10 | class Rune extends PureComponent { 11 | render() { 12 | const { slot } = this.props; 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Rune; 22 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const style = require('./index.scss'); 5 | 6 | interface ButtonGroupProps { 7 | className?: any; 8 | } 9 | 10 | const ButtonGroup: React.FC = ({ className, children }) => ( 11 |
12 |
{children}
13 |
14 | ); 15 | 16 | export default ButtonGroup; 17 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | $gold: rgb(240, 230, 210); 2 | $gold-medium: rgb(205, 190, 145); 3 | $gold-dark: rgb(200, 155, 60); 4 | 5 | $black: rgb(17, 22, 29); 6 | $rich-black: rgb(1, 11, 19); 7 | $gunmetal: rgb(30, 35, 40); 8 | 9 | $text-light: $gold; 10 | $text-dark: rgb(160, 155, 140); 11 | $text-disabled: rgb(92, 91, 87); 12 | 13 | $border: rgba(155, 125, 35, 0.5); 14 | $border-dark: rgb(70, 55, 20); 15 | $border-input: rgb(120, 90, 40); 16 | 17 | $bg-dark: rgb(1, 18, 35); 18 | -------------------------------------------------------------------------------- /src/components/Icons/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { t } from '@theme'; 4 | 5 | interface IconProps { 6 | color?: string; 7 | imageSrc: string; 8 | } 9 | 10 | export const Icon = styled.div` 11 | background-color: ${({ color = t.gunmetal }) => color}; 12 | background-image: url(${({ imageSrc = '' }) => imageSrc}); 13 | background-position: 50%; 14 | background-size: 60%; 15 | background-repeat: no-repeat; 16 | 17 | width: 28px; 18 | height: 28px; 19 | `; 20 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const config = require('../webpack.config'); 5 | 6 | delete config.externals; 7 | delete config.entry; 8 | delete config.output; 9 | 10 | const devMode = process.env.NODE_ENV !== 'production'; 11 | 12 | const plugins = [ 13 | new webpack.optimize.OccurrenceOrderPlugin(), 14 | 15 | new webpack.DefinePlugin({ 16 | 'process.env.NODE_ENV': JSON.stringify( 17 | devMode ? 'development' : 'production' 18 | ), 19 | }), 20 | ]; 21 | 22 | config.plugins = plugins; 23 | 24 | module.exports = config; 25 | -------------------------------------------------------------------------------- /src/components/Switcher/components/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames/bind'; 3 | import styled from 'styled-components'; 4 | 5 | import style from '../index.m.scss'; 6 | 7 | const cx = classnames.bind(style); 8 | 9 | const DefaultLink = styled.a` 10 | position: absolute; 11 | top: 0; 12 | bottom: 0; 13 | left: 0; 14 | right: 0; 15 | `; 16 | 17 | export const Item = ({ item, active, onClick }) => ( 18 |
  • 19 | {item.label} 20 | onClick(item)} /> 21 |
  • 22 | ); 23 | -------------------------------------------------------------------------------- /src/components/Dropdown/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-hextech 2 | 3 | A collection of React Components that implement Riot Games' [Hextech Visual Identity](https://www.behance.net/gallery/43098489/League-of-Legends-Hextech-Visual-Identity) 4 | 5 | # Contributing 6 | 7 | This library is targeting the web first. As such, it is important that all your work is responsive, does not sacrifice accessibility (screen reading and tab/arrow key navigation) and uses normal `` elements when possible. 8 | 9 | Another objective is to not use any of Riot's art assets directly, instead re-creating them using CSS and SVG (you may see some of these lying around in the repo, v1.0.0 won't until they're gone). 10 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | '@babel/preset-react', 3 | [ 4 | '@babel/preset-env', 5 | { 6 | useBuiltIns: 'entry', 7 | targets: { 8 | chrome: '58', 9 | ie: '11', 10 | }, 11 | modules: false, 12 | }, 13 | ], 14 | '@babel/preset-typescript', 15 | ]; 16 | 17 | const plugins = [ 18 | 'polished', 19 | ['styled-components', { ssr: true, displayName: true }], 20 | '@babel/plugin-proposal-class-properties', 21 | ]; 22 | 23 | if (process.env.NODE_ENV !== 'production') { 24 | plugins.push('babel-plugin-typescript-to-proptypes'); 25 | } 26 | 27 | module.exports = { 28 | presets, 29 | plugins, 30 | }; 31 | -------------------------------------------------------------------------------- /stories/Switcher.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Switcher as BaseSwitcher, stateful } from '../src'; 6 | 7 | const Switcher = stateful(BaseSwitcher); 8 | 9 | const options = ['Jinx', 'Leona', 'Renekton', 'Quinn'].map(label => ({ 10 | label, 11 | value: label, 12 | })); 13 | 14 | storiesOf('Switcher', module).add('normal', () => ( 15 | 22 | )); 23 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | /* eslint-disable */ 4 | import Ring from './Ring'; 5 | import Rune from './Rune'; 6 | import gradients from './gradients'; 7 | 8 | export default class RuneBuilder extends PureComponent { 9 | render() { 10 | return ( 11 | 19 | {gradients} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/Menu.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | const visible = css` 4 | max-height: 240px; 5 | opacity: 1; 6 | overflow-y: auto; 7 | `; 8 | 9 | const Menu = styled.div.attrs(({ hidden }) => ({ 10 | role: 'listbox', 11 | 'aria-hidden': hidden, 12 | }))` 13 | position: absolute; 14 | top: 100%; 15 | width: 100%; 16 | max-height: 0px; 17 | overflow-y: hidden; 18 | overflow-x: hidden; 19 | background: #010a13; 20 | opacity: 0; 21 | border: 1px solid #453617; 22 | border-top: none; 23 | z-index: 10; 24 | transition: all 0.4s ease; 25 | 26 | ${({ hidden }) => !hidden && visible} 27 | `; 28 | 29 | export default Menu; 30 | -------------------------------------------------------------------------------- /src/components/TextInput/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /stories/RadioInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { RadioInput as BaseRadioInput, stateful } from '../src'; 6 | 7 | const RadioInput = stateful(BaseRadioInput); 8 | 9 | const options = ['Jinx', 'Leona', 'Renekton', 'Quinn']; 10 | 11 | storiesOf('Radio Input', module) 12 | .add('with options', () => ( 13 | 14 | )) 15 | .add('disabled', () => ( 16 | 17 | )); 18 | -------------------------------------------------------------------------------- /src/components/Badge/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Icon, { icons } from '../Icons'; 4 | import * as S from './styled'; 5 | 6 | type IconProps = 7 | | { 8 | type: keyof typeof icons; 9 | } 10 | | { 11 | type: void; 12 | imageSrc?: string; 13 | color?: string; 14 | }; 15 | 16 | type BadgeProps = { 17 | text?: string; 18 | } & IconProps; 19 | 20 | const Badge: React.FC = ({ 21 | type, 22 | text, 23 | children, 24 | ...iconProps 25 | }) => ( 26 | 27 | 28 | {(text || children) && {text || children}} 29 | 30 | ); 31 | 32 | export default Badge; 33 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | export const palette = { 2 | gold: 'rgb(240, 230, 210)', 3 | goldMedium: 'rgb(205, 190, 145)', 4 | goldDark: 'rgb(200, 155, 60)', 5 | 6 | black: 'rgb(17, 22, 29)', 7 | richBlack: 'rgb(1, 11, 19)', 8 | gunmetal: 'rgb(30, 35, 40)', 9 | }; 10 | 11 | export const t = { 12 | ...palette, 13 | 14 | textLight: palette.gold, 15 | textDark: 'rgb(160, 155, 140)', 16 | textDisabled: 'rgb(92, 91, 87)', 17 | 18 | border: 'rgba(155, 125, 35, 0.5)', 19 | borderDark: 'rgb(69, 54, 23)', 20 | borderInput: 'rgb(120, 90, 40)', 21 | }; 22 | 23 | export const replacements = Object.keys(t).map(key => ({ 24 | search: `\W?t\.${key}\W?$`, 25 | replace: `'${t[key]}'`, 26 | flags: '', 27 | })); 28 | 29 | export default { 30 | hextech: t, 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/withOptions.ts: -------------------------------------------------------------------------------- 1 | import deburr from 'lodash.deburr'; 2 | import { withPropsOnChange } from 'recompose'; 3 | 4 | import { Option } from './option'; 5 | 6 | function normalizeOption(option: Option) { 7 | return Object.assign( 8 | {}, 9 | { 10 | value: option.value || option, 11 | label: option.label || option.value, 12 | // used for searching 13 | hextech__label: deburr(option.label || option).toLowerCase(), 14 | } 15 | ); 16 | } 17 | 18 | const normalizeOptions = options => { 19 | if (!options || !options.map) return options; 20 | return options.map(normalizeOption); 21 | }; 22 | 23 | export const withOptions = withPropsOnChange(['options'], ({ options }) => ({ 24 | options: normalizeOptions(options), 25 | })); 26 | -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Badge from '../Badge'; 4 | 5 | import * as S from './styled'; 6 | 7 | const Card = ({ badge = {} }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

    Test

    16 |

    Hello World

    17 | 18 |

    19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. In 20 | imperdiet, mi eu sodales mattis, ante nisi sagittis orci. 21 |

    22 |
    23 |
    24 | 25 | 26 | Story 27 | 28 | 29 |
    30 |
    31 | ); 32 | 33 | export default Card; 34 | -------------------------------------------------------------------------------- /src/components/Switcher/index.m.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .wrapper { 4 | position: relative; 5 | } 6 | 7 | .bar { 8 | height: 2px; 9 | margin-top: -2px; 10 | transition: transform 0.4s ease, width 0.2s ease; 11 | background: $border-input; 12 | will-change: transform, width; 13 | 14 | transform: translateX(0px); 15 | width: 0px; 16 | } 17 | 18 | .list { 19 | display: flex; 20 | align-items: center; 21 | list-style: none; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | .item { 27 | position: relative; 28 | list-style: none; 29 | text-transform: capitalize; 30 | padding: 20px 14px; 31 | color: $gold-medium; 32 | transition: color 0.4s ease; 33 | letter-spacing: 0.2px; 34 | 35 | &:hover, 36 | &.active { 37 | color: $text-light; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const ArrowSvg = props => ( 5 | 14 | 15 | 16 | 17 | ); 18 | 19 | const Arrow = styled(ArrowSvg)` 20 | fill: #c8aa6e; 21 | position: absolute; 22 | top: 0; 23 | bottom: 0; 24 | right: 9px; 25 | margin: auto; 26 | cursor: pointer; 27 | height: 1em; 28 | `; 29 | 30 | export default Arrow; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log* 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | app/node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | docs 36 | docs/static 37 | lib 38 | 39 | # Visual studio code 40 | .vscode/ 41 | -------------------------------------------------------------------------------- /stories/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Checkbox as BaseCheckbox, stateful } from '../src'; 6 | 7 | const Checkbox = stateful(BaseCheckbox); 8 | 9 | storiesOf('Checkbox', module) 10 | .add('with text', () => ( 11 | 17 | Hextech 18 | 19 | )) 20 | .add('disabled', () => ( 21 | 26 | Try Me 27 | 28 | )); 29 | -------------------------------------------------------------------------------- /src/components/Badge/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { t } from '@theme'; 4 | 5 | import Icon from '../Icons'; 6 | 7 | export const Text = styled.h6` 8 | background-color: ${t.gunmetal}; 9 | border: 2px solid #3c3c41; 10 | border-left: none; 11 | overflow: hidden; 12 | padding: 6px 7px; 13 | 14 | font-family: 'Spiegel', Helvetica, sans-serif; 15 | font-weight: 700; 16 | letter-spacing: 0.05em; 17 | line-height: 100%; 18 | text-transform: uppercase; 19 | font-size: 12px; 20 | color: #a09b8c; 21 | text-overflow: ellipsis; 22 | text-transform: capitalize; 23 | 24 | white-space: nowrap; 25 | `; 26 | 27 | export const Wrapper = styled.div` 28 | position: relative; 29 | max-height: 28px; 30 | overflow: hidden; 31 | 32 | ${Text}, ${Icon.styled} { 33 | display: inline-block; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "testRegex": "(/__tests__/.*|(\\.|/)spec)\\.jsx?$", 4 | "snapshotSerializers": ["enzyme-to-json/serializer"], 5 | "setupTestFrameworkScriptFile": "./setup-tests.js", 6 | "moduleFileExtensions": ["js", "jsx", "test.js", "json"], 7 | "moduleDirectories": ["node_modules"], 8 | "moduleNameMapper": { 9 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 10 | "\\.(css|less|scss)$": "identity-obj-proxy" 11 | }, 12 | "collectCoverageFrom": [ 13 | "src/**/*.{js}", 14 | "src/**/*.{jsx}", 15 | "!**/node_modules/**", 16 | "!**/lib/**", 17 | "!**/dist/**", 18 | "!**/*.test.js", 19 | "!**/__tests__/**" 20 | ], 21 | "coverageReporters": ["text", "html"], 22 | "coveragePathIgnorePatterns": ["/lib/", "/node_modules/"] 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/index.scss: -------------------------------------------------------------------------------- 1 | .buttonGroup { 2 | display: inline-flex; 3 | 4 | .content { 5 | position: relative; 6 | background-color: #010A13; 7 | padding: 0 4px; 8 | display: flex; 9 | 10 | & > div { 11 | margin: 0 2px; 12 | 13 | &:first-child { 14 | margin-left: 0; 15 | } 16 | 17 | &:last-child { 18 | margin-right: 0; 19 | } 20 | } 21 | 22 | &::before { 23 | content: ''; 24 | position: absolute; 25 | left: 0; 26 | bottom: 0; 27 | border-right: 2px solid #614A1F; 28 | border-top: 2px solid transparent; 29 | height: 10px; 30 | } 31 | 32 | &::after { 33 | content: ''; 34 | position: absolute; 35 | right: 0; 36 | bottom: 0; 37 | border-left: 2px solid #614A1F; 38 | border-top: 2px solid transparent; 39 | height: 10px; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Badge from './components/Badge'; 2 | import Button from './components/Button'; 3 | import ButtonGroup from './components/ButtonGroup'; 4 | import Card from './components/Card'; 5 | import Checkbox from './components/Checkbox'; 6 | import Dropdown from './components/Dropdown'; 7 | import Switcher from './components/Switcher'; 8 | import Frame from './components/Frame'; 9 | import TextInput from './components/TextInput'; 10 | import RadioInput from './components/RadioInput'; 11 | import SliderInput from './components/SliderInput'; 12 | import RuneBuilder from './components/RuneBuilder'; 13 | import stateful from './utils/stateful'; 14 | 15 | import './css/global.scss'; 16 | 17 | export { 18 | Badge, 19 | Button, 20 | ButtonGroup, 21 | Card, 22 | Checkbox, 23 | Dropdown, 24 | Switcher, 25 | Frame, 26 | TextInput, 27 | RadioInput, 28 | SliderInput, 29 | RuneBuilder, 30 | stateful, 31 | }; 32 | -------------------------------------------------------------------------------- /stories/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Button } from '../src'; 6 | 7 | storiesOf('Button', module) 8 | .add('with text', () => ( 9 | 10 | )) 11 | .add('disabled', () => ( 12 | 15 | )) 16 | .add('alternate', () => ) 17 | .add('alternate (disabled)', () => ( 18 | 21 | )) 22 | .add('alternate (magic)', () => ( 23 | 24 | )) 25 | .add('Pure CSS', () => ) 26 | .add('Pure CSS (disabled)', () => ); 27 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/Body/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = require('./index.scss'); 4 | 5 | const Rune = () => ( 6 | 7 | 8 | 12 | 13 | 14 | 18 | 22 | 26 | 27 | 28 | ); 29 | 30 | export default Rune; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "outDir": "lib", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "allowSyntheticDefaultImports": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "checkJs": true, 13 | "rootDir": "src", 14 | "allowJs": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@theme": ["src/theme.js"], 18 | "@utils": ["src/utils/index.ts"], 19 | "@utils/*": ["src/utils/*"], 20 | "@assets/*": ["src/assets/*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "typescript-plugin-css-modules", 25 | "options": { 26 | "customMatcher": "\\.m\\.scss$", 27 | "camelCase": "dashes" 28 | } 29 | } 30 | ] 31 | }, 32 | "include": ["src"], 33 | "exclude": ["src/components/__tests__", "src/theme.js"] 34 | } 35 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/RadioInput.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render correctly 1`] = ` 4 | 39 | `; 40 | -------------------------------------------------------------------------------- /src/utils/styled.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a variable from the hextech theme 3 | * 4 | * @param {string} name hextech theme variable name 5 | */ 6 | export const v = name => ({ theme }) => theme.hextech[name]; 7 | 8 | /** 9 | * Ternary hextech theme variable getter 10 | * 11 | * @param {Function} predicate predicate to test with component props 12 | * @param {string} left variable name if predicate is true 13 | * @param {*} right variable name if predicate is false 14 | * @returns {(p: P) => any} props 15 | */ 16 | export const vif = (predicate, left, right) => props => { 17 | const hextech = props.theme.hextech; 18 | 19 | return predicate(props) ? hextech[left] : hextech[right]; 20 | }; 21 | 22 | /** 23 | * Gets a prop from the component and optionally appends a css unit 24 | * 25 | * @param {string} name component prop name 26 | * @param {'px' | 'em' | 'pt' | 'rem' | 'vh' | 'vw'} unit CSS unit 27 | */ 28 | export const p = (name, unit = '') => props => `${props[name]}${unit}`; 29 | -------------------------------------------------------------------------------- /src/components/Switcher/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { t } from '@theme'; 4 | 5 | export const Bar = styled.div<{ x: number; width: number }>` 6 | height: 2px; 7 | margin-top: -2px; 8 | transition: transform 0.4s ease, width 0.2s ease; 9 | background: ${t.borderInput}; 10 | 11 | transform: ${({ x = 0 }) => `translateX(${x}px)`}; 12 | width: ${({ width = 0 }) => width}px; 13 | `; 14 | 15 | export const List = styled.ul` 16 | display: flex; 17 | align-items: center; 18 | list-style: none; 19 | margin: 0; 20 | padding: 0; 21 | `; 22 | 23 | export const Li = styled.li<{ active: boolean }>` 24 | position: relative; 25 | list-style: none; 26 | text-transform: capitalize; 27 | padding: 20px 14px; 28 | color: ${({ active }) => (active ? t.textLight : t.goldMedium)}; 29 | transition: color 0.4s ease; 30 | letter-spacing: 0.2px; 31 | 32 | :hover { 33 | color: ${t.textLight}; 34 | } 35 | `; 36 | 37 | export const Wrapper = styled.div` 38 | position: relative; 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | import { t } from '@theme'; 4 | 5 | import Arrow from './Arrow'; 6 | 7 | const disabledStyles = css` 8 | pointer-events: none; 9 | cursor: default; 10 | color: ${t.textDisabled}; 11 | 12 | .control { 13 | border-image: none; 14 | border-color: ${t.textDisabled}; 15 | background-color: ${t.gunmetal}; 16 | 17 | ${Arrow} { 18 | fill: ${t.textDisabled}; 19 | } 20 | } 21 | `; 22 | 23 | const Wrapper = styled.div.attrs(({ disabled, open }) => ({ 24 | role: 'combobox', 25 | 'aria-expanded': open, 26 | 'aria-disabled': disabled, 27 | }))` 28 | display: block; 29 | position: relative; 30 | cursor: pointer; 31 | width: 100%; 32 | min-width: 130px; 33 | font-family: 'Spiegel'; 34 | color: rgb(80.4%, 74.5%, 56.9%); 35 | letter-spacing: 0.025rem; 36 | 37 | &, 38 | & div { 39 | box-sizing: border-box; 40 | margin: 0; 41 | } 42 | 43 | ${({ disabled }) => disabled && disabledStyles} 44 | `; 45 | 46 | export default Wrapper; 47 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/Slot/index.scss: -------------------------------------------------------------------------------- 1 | .slot.quintessence { 2 | &:hover { 3 | .slotInner { 4 | fill: url(#quintHighlightInner); 5 | } 6 | .slotMiddle { 7 | fill: url(#quintHighlight); 8 | // stroke: url(#quintHighlight); 9 | } 10 | } 11 | &:active { 12 | .slotOuter { 13 | fill: url(#quintHover); 14 | } 15 | } 16 | 17 | .slotOuter { 18 | fill: #010a13; 19 | stroke: #6B5024; 20 | } 21 | .slotInner { 22 | // fill: #0E1116; 23 | fill: url(#slotInner); 24 | } 25 | .slotMiddle { 26 | fill: #211F22; 27 | stroke: #15171B; 28 | } 29 | } 30 | 31 | .rune.quintessence { 32 | .slotInner { 33 | fill: #0e141e; 34 | } 35 | 36 | .slotMiddle { 37 | fill: url(#quintMiddle); 38 | } 39 | 40 | .slotOuter { 41 | fill: url(#quintOuter); 42 | stroke: url(#quintOuterStroke); 43 | stroke-width: 7px; 44 | } 45 | } 46 | 47 | .slotInner { 48 | stroke: #15171B; 49 | stroke-width: 2px; 50 | } 51 | 52 | .slotOuter { 53 | stroke-width: 2px; 54 | stroke-miterlimit: 10; 55 | } 56 | 57 | .slotMiddle { 58 | stroke-miterlimit: 10; 59 | } 60 | -------------------------------------------------------------------------------- /stories/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import { TextInput as BaseTextInput, stateful } from '../src'; 5 | 6 | const TextInput = stateful(BaseTextInput); 7 | 8 | storiesOf('Text Input', module) 9 | .add('normal', () => ( 10 | 11 | )) 12 | .add('search', () => ( 13 | 14 | )) 15 | .add('without clear button', () => ( 16 | 17 | )) 18 | .add('disabled', () => ( 19 | 20 | )) 21 | .add('native', () => ( 22 | 23 | )) 24 | .add('native (with search)', () => ( 25 | 26 | )); 27 | -------------------------------------------------------------------------------- /src/components/Icons/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as S from './styled'; 4 | 5 | const storyPng = require('@assets/icons/story.png'); 6 | const mediaPng = require('@assets/icons/media.png'); 7 | const comicPng = require('@assets/icons/comic.png'); 8 | 9 | export const icons = { 10 | story: { 11 | imageSrc: storyPng, 12 | color: 'rgb(30, 130, 90)', 13 | }, 14 | media: { 15 | imageSrc: mediaPng, 16 | color: 'rgb(190, 30, 55)', 17 | }, 18 | comic: { 19 | imageSrc: comicPng, 20 | color: 'rgb(119, 10, 89)', 21 | }, 22 | }; 23 | 24 | type Icons = keyof typeof icons; 25 | 26 | type IconProps = 27 | | { 28 | name: Icons; 29 | } 30 | | { 31 | name?: void; 32 | imageSrc: string; 33 | color?: string; 34 | }; 35 | 36 | type IconType = React.FC & { styled: typeof S.Icon }; 37 | 38 | const Icon: IconType = ({ name, ...iconProps }) => { 39 | const icon = name ? icons[name] : (iconProps as any); 40 | 41 | return ; 42 | }; 43 | 44 | Icon.styled = S.Icon; 45 | 46 | export default Icon; 47 | -------------------------------------------------------------------------------- /src/utils/stateful.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | function stateful

    (Cmp: React.ComponentType

    ) { 4 | interface StatefulProps { 5 | initialValue: any; 6 | hocClassName?: any; 7 | onChange?: (nextValue: T) => void; 8 | } 9 | 10 | return class extends PureComponent

    { 11 | static displayName = `stateful(${Cmp.displayName || Cmp.name})`; 12 | 13 | state = { 14 | value: undefined, 15 | }; 16 | 17 | handleChange = value => { 18 | const { onChange } = this.props; 19 | this.setState({ value }); 20 | if (onChange && onChange.call) { 21 | onChange(value); 22 | } 23 | }; 24 | 25 | render() { 26 | const { hocClassName, initialValue, ...rest } = this.props; 27 | 28 | return ( 29 |

    30 | 35 |
    36 | ); 37 | } 38 | }; 39 | } 40 | 41 | export default stateful; 42 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Checkbox.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render correctly 1`] = ` 4 |
    12 |
    15 |
    18 | 21 |
    22 | 25 | Test 1 26 | 27 |
    28 | `; 29 | 30 | exports[` should render correctly 2`] = ` 31 |
    39 |
    42 |
    45 | 48 |
    49 | 52 | Test 2 53 | 54 |
    55 | `; 56 | -------------------------------------------------------------------------------- /stories/Frame.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Frame, RadioInput as BaseRadioInput, stateful } from '../src'; 6 | 7 | const RadioInput = stateful(BaseRadioInput); 8 | 9 | const acceptClick = action('button-accept'); 10 | const rejectClick = action('button-reject'); 11 | 12 | storiesOf('Frame', module) 13 | .add('with title and body', () => ( 14 |

    A React Component library that aims to re-create the Hextech look of League of Legends

    15 | )) 16 | .add('with custom body', () => ( 17 | 18 |

    Favorite Champion

    19 | 20 | 21 | )) 22 | .add('with buttons', () => ( 23 | 24 |

    Are you Challenger?

    25 |

    This is a question of utmost importance, please answer honestly

    26 | 27 | )); 28 | -------------------------------------------------------------------------------- /stories/SliderInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import debounce from 'lodash.debounce'; 5 | 6 | import { SliderInput as BaseSliderInput, stateful } from '../src'; 7 | 8 | const SliderInput = stateful(BaseSliderInput); 9 | 10 | storiesOf('Slider Input', module) 11 | .add('normal', () => ( 12 | 13 | )) 14 | .add('normal (step=1)', () => ( 15 | 16 | )) 17 | .add('with tooltip [TODO]', () => ( 18 | 19 | )) 20 | .add('disabled', () => ( 21 | 22 | )); 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present S. Coimbra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/css/input.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | 3 | :global { 4 | input.hextech { 5 | font-family: 'Spiegel'; 6 | padding: 10px 21px; 7 | color: $text-dark; 8 | display: block; 9 | box-sizing: border-box; 10 | border: 1px solid #785a28; 11 | background-color: rgba(0, 0, 0, 0.7); 12 | appearance: none; 13 | outline: none; 14 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 15 | 0 0 0 1px rgba(0, 0, 0, 0.25); 16 | 17 | &:focus { 18 | background: linear-gradient( 19 | to bottom, 20 | rgba(7, 16, 25, 0.7), 21 | rgba(32, 39, 44, 0.7) 22 | ); 23 | border-image: linear-gradient(to bottom, #785a28, #c8aa6e) 1 stretch; 24 | } 25 | 26 | &:placeholder-shown { 27 | font-size: 1em; 28 | } 29 | 30 | &[type='search'] { 31 | background-image: url('../assets/search-icon.png'); 32 | background-size: 1.3em; 33 | background-position: 5px center; 34 | background-repeat: no-repeat; 35 | padding-left: 1.8em; 36 | 37 | &:focus { 38 | background: 5px center/1.3em url('../assets/search-icon.png'), 39 | linear-gradient( 40 | to bottom, 41 | rgba(7, 16, 25, 0.7), 42 | rgba(32, 39, 44, 0.7) 43 | ); 44 | background-repeat: no-repeat; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Ring/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = require('./index.scss'); 4 | 5 | const Ring = () => ( 6 | 7 | 14 | 21 | 28 | 35 | 36 | 43 | 50 | 51 | 52 | ); 53 | 54 | export default Ring; 55 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/Option.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | const checkSvg = require('./check.svg'); 4 | 5 | const focusStyles = css` 6 | color: rgb(94.1%, 90.2%, 82.4%); 7 | background-color: rgb(11.8%, 13.7%, 15.7%); 8 | `; 9 | 10 | const selectedStyles = css` 11 | padding-right: 1.25em; 12 | padding-right: calc(1.25em + 10px); 13 | 14 | ::after { 15 | content: ''; 16 | position: absolute; 17 | right: 10px; 18 | top: 0; 19 | bottom: 0; 20 | background: url(${checkSvg}) center no-repeat; 21 | width: 1.25em; 22 | margin: auto; 23 | } 24 | `; 25 | 26 | const Option = styled.div.attrs(({ selected }) => ({ 27 | role: 'options', 28 | 'aria-selected': selected, 29 | }))` 30 | position: relative; 31 | cursor: pointer; 32 | overflow-x: hidden; 33 | padding: 10px 14px; 34 | padding-right: 0; 35 | height: 40px; 36 | border-top: 1px solid rgb(12.2%, 12.9%, 13.7%); 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | 40 | ${({ focused }) => focused && focusStyles} 41 | ${({ selected }) => selected && selectedStyles} 42 | 43 | :active { 44 | color: rgb(27.5%, 21.6%, 7.8%); 45 | background-color: rgba(30, 35, 40, 0.5); 46 | } 47 | `; 48 | 49 | export default Option; 50 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/Rune/Slot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import slots from './slots'; 5 | const style = require('./index.scss'); 6 | 7 | const RuneSlot = ({ slot, children }) => { 8 | const slotData = slots[slot]; 9 | 10 | return ( 11 | = 27 ? '156px' : '50px'} 14 | height={slot >= 27 ? '159px' : '56.8px'} 15 | viewBox="0 0 425 433" 16 | enableBackground="new 0 0 425 433" 17 | {...slotData} 18 | > 19 | 20 | 24 | 28 | 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export default RuneSlot; 39 | -------------------------------------------------------------------------------- /stories/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Dropdown as BaseDropdown, stateful } from '../src'; 6 | 7 | const Dropdown = stateful(BaseDropdown); 8 | 9 | const options = ['Jinx', 'Leona', 'Renekton', 'Quinn']; 10 | const lotsOptions = [ 11 | ...options, 12 | 'Anivia', 13 | 'Tristana', 14 | 'Fiora', 15 | 'Vayne', 16 | 'Tryndamere', 17 | 'Yasuo', 18 | 'Poppy', 19 | 'Diana', 20 | ]; 21 | 22 | storiesOf('Dropdown', module) 23 | .add('normal', () => ( 24 | 31 | )) 32 | .add('transparent', () => ( 33 | 40 | )) 41 | .add('with scroll [TODO]', () => ( 42 | 48 | )) 49 | .add('disabled [TODO]', () => ( 50 | 57 | )); 58 | -------------------------------------------------------------------------------- /src/css/global.scss: -------------------------------------------------------------------------------- 1 | @import './button.scss'; 2 | @import './input.scss'; 3 | 4 | a { 5 | text-decoration: none; 6 | outline: none; 7 | -webkit-tap-highlight-color: transparent; 8 | } 9 | 10 | html, 11 | body, 12 | div, 13 | span, 14 | applet, 15 | object, 16 | iframe, 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6, 23 | p, 24 | blockquote, 25 | pre, 26 | a, 27 | abbr, 28 | acronym, 29 | address, 30 | big, 31 | cite, 32 | code, 33 | del, 34 | dfn, 35 | em, 36 | img, 37 | ins, 38 | kbd, 39 | q, 40 | s, 41 | samp, 42 | small, 43 | strike, 44 | strong, 45 | sub, 46 | sup, 47 | tt, 48 | var, 49 | b, 50 | u, 51 | i, 52 | center, 53 | dl, 54 | dt, 55 | dd, 56 | ol, 57 | ul, 58 | li, 59 | fieldset, 60 | form, 61 | label, 62 | legend, 63 | table, 64 | caption, 65 | tbody, 66 | tfoot, 67 | thead, 68 | tr, 69 | th, 70 | td, 71 | article, 72 | aside, 73 | canvas, 74 | details, 75 | embed, 76 | figure, 77 | figcaption, 78 | footer, 79 | header, 80 | hgroup, 81 | menu, 82 | nav, 83 | output, 84 | ruby, 85 | section, 86 | summary, 87 | time, 88 | mark, 89 | audio, 90 | video { 91 | margin: 0; 92 | padding: 0; 93 | border: 0; 94 | font-size: 100%; 95 | font: inherit; 96 | vertical-align: baseline; 97 | } 98 | 99 | html, 100 | body { 101 | font-family: 'Spiegel', sans-serif; 102 | font-size: 14px; 103 | } 104 | 105 | h1, 106 | h2, 107 | h3, 108 | h4, 109 | h5, 110 | h6 { 111 | font-family: 'Beaufort', Arial, Helvetica, sans-serif; 112 | font-weight: bold; 113 | } 114 | 115 | h1 { 116 | font-size: 200%; 117 | } 118 | 119 | h2 { 120 | font-size: 150%; 121 | } 122 | 123 | h3 { 124 | font-size: 117%; 125 | } 126 | -------------------------------------------------------------------------------- /src/components/__tests__/RadioOption.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | 4 | import RadioOption from '../RadioOption'; 5 | 6 | describe('', () => { 7 | it('should render two children', () => { 8 | const wrapper = shallow(); 9 | expect(wrapper.children().length).toBe(2); 10 | }); 11 | 12 | it('should render its label using props.label', () => { 13 | const o = { label: 'testlabel', value: 'testvalue' }; 14 | const wrapper = mount( 15 | false} {...o} /> 16 | ); 17 | expect(wrapper.find('.labelText').text()).toBe(o.label); 18 | }); 19 | 20 | it('should render its label using props.children', () => { 21 | const child = 'testchild'; 22 | const o = { value: 'testvalue' }; 23 | const wrapper = mount( 24 | false} {...o}> 25 | {child} 26 | 27 | ); 28 | expect(wrapper.find('.label').text()).toBe('testchild'); 29 | }); 30 | 31 | it('should have the class "checked" if it is checked', () => { 32 | const wrapper = shallow(); 33 | expect(wrapper.hasClass('checked')).toBe(true); 34 | }); 35 | 36 | it('should not have the class "checked" if it isn\'t checked', () => { 37 | const wrapper = shallow( 38 | 39 | ); 40 | expect(wrapper.hasClass('checked')).toBe(false); 41 | }); 42 | 43 | it('should have the class "disabled" if it is disabled', () => { 44 | const wrapper = shallow( 45 | 46 | ); 47 | expect(wrapper.hasClass('disabled')).toBe(true); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const style = require('./index.scss'); 5 | 6 | interface TextInputProps { 7 | className: string; 8 | inputClassName: string; 9 | tabIndex: number; 10 | value: string; 11 | type: string; 12 | disabled: boolean; 13 | placeholder: string; 14 | hideClear: boolean; 15 | onBlur: () => void; 16 | onChange: (nextValue: string) => void; 17 | } 18 | 19 | class TextInput extends PureComponent { 20 | static defaultProps = { 21 | value: '', 22 | type: 'text', 23 | disabled: false, 24 | hideClear: false, 25 | }; 26 | 27 | handleChange = evt => { 28 | evt.preventDefault(); 29 | 30 | const { disabled, onChange } = this.props; 31 | if (!disabled && onChange) { 32 | onChange(evt.target.value); 33 | } 34 | }; 35 | 36 | handleClear = () => { 37 | const { onChange } = this.props; 38 | if (onChange) { 39 | onChange(''); 40 | } 41 | }; 42 | 43 | render() { 44 | const { 45 | className, 46 | inputClassName, 47 | disabled, 48 | value, 49 | hideClear, 50 | children, 51 | ...inputProps 52 | } = this.props; 53 | 54 | const showClear = !hideClear && value !== ''; 55 | 56 | return ( 57 |
    58 | 62 | 69 | {children} 70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default TextInput; 76 | -------------------------------------------------------------------------------- /src/components/RadioInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import { withOptions } from '@utils'; 5 | 6 | import RadioOption from '../RadioOption'; 7 | import { Option } from '../../utils/option'; 8 | 9 | const style = require('./index.scss'); 10 | 11 | interface RadioInputProps { 12 | className?: any; 13 | disabled?: boolean; 14 | value: any; 15 | label?: string; 16 | options: Option[]; 17 | onChange: (nextValue: any) => void; 18 | onBlur?: () => void; 19 | } 20 | 21 | class RadioInput extends PureComponent { 22 | static defaultProps = { 23 | className: undefined, 24 | disabled: false, 25 | value: undefined, 26 | label: '', 27 | options: [], 28 | onChange: Function.prototype, 29 | onBlur: Function.prototype, 30 | }; 31 | 32 | handleSelect = (nextValue, evt) => { 33 | evt.preventDefault(); 34 | const { value, onChange } = this.props; 35 | 36 | if (nextValue !== value) { 37 | onChange(nextValue); 38 | } 39 | }; 40 | 41 | render() { 42 | const { options, className, disabled, value, label, onBlur } = this.props; 43 | 44 | return ( 45 |
    50 | {options.map((o, i) => ( 51 | this.handleSelect(o.value, evt)} 57 | onBlur={onBlur} 58 | label={o.label} 59 | value={o.value} 60 | tabIndex={i > 0 ? -1 : 0} 61 | /> 62 | ))} 63 |
    64 | ); 65 | } 66 | } 67 | 68 | export default withOptions(RadioInput); 69 | -------------------------------------------------------------------------------- /src/components/Frame/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import ButtonGroup from '../ButtonGroup'; 5 | import Button from '../Button'; 6 | import { Option } from '../../utils/option'; 7 | 8 | const style = require('./index.scss'); 9 | 10 | interface FrameProps { 11 | // Visual 12 | className: string; 13 | contentClassName: string; 14 | borders: object; 15 | // State 16 | options: Option; 17 | title: string; 18 | message: string; 19 | } 20 | 21 | export default class Frame extends PureComponent { 22 | static defaultProps = { 23 | className: undefined, 24 | contentClassName: undefined, 25 | borders: { 26 | top: true, 27 | bottom: true, 28 | left: false, 29 | right: false, 30 | }, 31 | options: undefined, 32 | title: undefined, 33 | message: undefined, 34 | }; 35 | 36 | render() { 37 | const { 38 | className, 39 | contentClassName, 40 | borders, 41 | options, 42 | title, 43 | message, 44 | children, 45 | } = this.props; 46 | 47 | return ( 48 |
    49 |
    50 |
    51 | {title &&

    {title}

    } 52 | {!children &&

    {message}

    } 53 | {children} 54 |
    55 | {options && ( 56 | 57 | {options.map(o => ( 58 |
    63 |
    v && style[s]) 67 | )} 68 | /> 69 |
    70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Frame/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .frame { 4 | border: 2px solid transparent; 5 | border-image: linear-gradient(to top, #614a1f 0, #463714 5px, #463714 100%) 1 6 | stretch; 7 | position: relative; 8 | background: rgb(0.4%, 3.9%, 7.5%); 9 | box-shadow: 0 0 0 1px rgba(1, 10, 19, 0.48); 10 | box-sizing: border-box; 11 | 12 | &::before { 13 | content: ''; 14 | position: absolute; 15 | width: calc(100% + 4px); 16 | height: calc(100% + 4px); 17 | top: -2px; 18 | left: -2px; 19 | box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.5); 20 | pointer-events: none; 21 | } 22 | 23 | .contentWrapper { 24 | .content { 25 | color: $text-dark; 26 | padding: 18px; 27 | width: 100%; 28 | text-align: center; 29 | box-sizing: border-box; 30 | font-family: 'Beaufort'; 31 | 32 | & h2 { 33 | color: $gold; 34 | font-weight: 700; 35 | line-height: 22px; 36 | letter-spacing: 0.05rem; 37 | margin-top: -2px; 38 | } 39 | 40 | & p { 41 | font-weight: normal; 42 | line-height: 1rem; 43 | letter-spacing: 0.025rem; 44 | margin: 0; 45 | font-family: 'Spiegel'; 46 | 47 | &:last-child { 48 | margin-bottom: -2px; 49 | } 50 | } 51 | } 52 | 53 | .buttonGroup { 54 | margin-bottom: -2px; 55 | width: 100%; 56 | justify-content: center; 57 | } 58 | } 59 | 60 | .border { 61 | &::before, 62 | &::after { 63 | position: absolute; 64 | box-sizing: border-box; 65 | left: 12px; 66 | right: 12px; 67 | height: 0; 68 | border-style: solid; 69 | border-width: 4px 4px 0 4px; 70 | border-image-width: 4px 4px 0 4px; 71 | border-image-slice: 4 4 0 4; 72 | border-image-repeat: stretch; 73 | } 74 | 75 | &.top::before { 76 | content: ''; 77 | top: -6px; 78 | border-image-source: url('./sub-border-secondary-horizontal.png'); 79 | } 80 | 81 | &.bottom::after { 82 | content: ''; 83 | bottom: -6px; 84 | border-image-source: url('./sub-border-primary-horizontal.png'); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/RadioOption/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import cx from 'classnames'; 4 | 5 | const style = require('./index.scss'); 6 | 7 | const RadioOption: React.FC> = ({ 8 | children, 9 | name, 10 | value, 11 | label, 12 | checked, 13 | disabled, 14 | onChange, 15 | onBlur, 16 | }) => ( 17 |
    24 | 34 | 56 |
    57 | ); 58 | 59 | RadioOption.propTypes = { 60 | children: PropTypes.node, 61 | name: PropTypes.string, 62 | value: PropTypes.any.isRequired, 63 | label: PropTypes.string, 64 | disabled: PropTypes.bool, 65 | checked: PropTypes.bool, 66 | onChange: PropTypes.func.isRequired, 67 | onBlur: PropTypes.func, 68 | }; 69 | 70 | RadioOption.defaultProps = { 71 | children: undefined, 72 | name: undefined, 73 | onBlur: undefined, 74 | onChange: () => false, 75 | label: undefined, 76 | checked: false, 77 | disabled: false, 78 | }; 79 | 80 | export default RadioOption; 81 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure } from '@storybook/react'; 3 | import { setOptions } from '@storybook/addon-options'; 4 | import { addDecorator } from '@storybook/react'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import { withBackgrounds } from '@storybook/addon-backgrounds'; 7 | import { checkA11y } from '@storybook/addon-a11y'; 8 | 9 | import './index.css'; 10 | import theme from '../src/theme'; 11 | 12 | addDecorator(checkA11y); 13 | addDecorator( 14 | withBackgrounds([ 15 | { name: 'LCU', value: 'var(--lcu-bg)', default: true }, 16 | { name: 'Universe', value: 'var(--universe-bg)' }, 17 | { name: 'twitter', value: '#00aced' }, 18 | ]) 19 | ); 20 | // addDecorator( 21 | // withCssResources({ 22 | // cssresources: [ 23 | // // { 24 | // // name: 'LCU', 25 | // // code: ``, 26 | // // picked: true, 27 | // // }, 28 | // { 29 | // name: 'Universe', 30 | // code: ``, 31 | // }, 32 | // ], 33 | // }) 34 | // ); 35 | 36 | const Theme = storyFn => ( 37 | {storyFn()} 38 | ); 39 | 40 | setOptions({ 41 | name: 'React-Hextech', 42 | url: 'https://github.com/LeagueDevelopers/react-hextech', 43 | goFullScreen: false, 44 | showLeftPanel: true, 45 | showDownPanel: true, 46 | showSearchBox: false, 47 | downPanelInRight: false, 48 | sortStoriesByKind: false, 49 | }); 50 | 51 | function loadStories() { 52 | require('../stories/Badge.js'); 53 | require('../stories/Button.js'); 54 | require('../stories/Checkbox.jsx'); 55 | require('../stories/Card.js'); 56 | require('../stories/Dropdown.js'); 57 | require('../stories/Switcher.js'); 58 | require('../stories/TextInput.js'); 59 | require('../stories/RadioInput.js'); 60 | require('../stories/SliderInput.js'); 61 | require('../stories/Frame.js'); 62 | require('../stories/Todo.js'); 63 | // You can require as many stories as you need. 64 | } 65 | 66 | addDecorator(Theme); 67 | 68 | configure(loadStories, module); 69 | -------------------------------------------------------------------------------- /src/components/RuneBuilder/gradients.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* eslint-disable */ 4 | 5 | export default [ 6 | /* Rune Slot */ 7 | 8 | 9 | 10 | , 11 | 12 | 13 | 14 | 15 | , 16 | 17 | 18 | 19 | 20 | , 21 | 22 | 23 | 24 | , 25 | /* Quintessence */ 26 | 27 | 28 | 29 | 30 | , 31 | 32 | 33 | 34 | , 35 | 36 | 37 | 38 | , 39 | /* Quintessence Body */ 40 | 48 | 49 | 50 | 51 | 52 | , 53 | 61 | 62 | 63 | , 64 | ]; 65 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const style = require('./index.scss'); 5 | 6 | interface CheckboxProps { 7 | // Visual 8 | className: any; 9 | label: string; 10 | // State 11 | value: boolean; 12 | disabled: boolean; 13 | // Events 14 | onFocus: React.FormEventHandler; 15 | onChange: (nextValue: T) => void; 16 | onClick: React.MouseEventHandler; 17 | onBlur: React.FormEventHandler; 18 | tabIndex: number; 19 | } 20 | 21 | /** 22 | * 23 | * 24 | * An uncontrolled checkbox. 25 | * onClick is called with the click event, 26 | * onChange is called with next value 27 | * 28 | */ 29 | export default class Checkbox extends PureComponent { 30 | static defaultProps = { 31 | className: undefined, 32 | label: '', 33 | value: false, 34 | disabled: false, 35 | onChange: () => false, 36 | onClick: () => false, 37 | onBlur: () => false, 38 | tabIndex: '0', 39 | children: undefined, 40 | }; 41 | 42 | input = React.createRef(); 43 | 44 | handleClick = evt => { 45 | const { value, onClick, onChange } = this.props; 46 | 47 | if (evt.target !== this.input.current) { 48 | this.input.current.focus(); 49 | 50 | onClick(evt); 51 | } 52 | 53 | onChange(!value); 54 | }; 55 | 56 | render() { 57 | const { className, value, label, onBlur, onFocus, children } = this.props; 58 | const { disabled } = this.props; 59 | 60 | const isChecked = !!value; 61 | const classes = [ 62 | isChecked && style.checked, 63 | disabled && style.disabled, 64 | className, 65 | ]; 66 | 67 | return ( 68 |
    74 | 80 |
    81 |
    82 |
    83 | 84 |
    85 | {children || label} 86 |
    87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react", "tslint-config-prettier"], 3 | "rules": { 4 | "jsx-wrap-multiline": false, 5 | "align": [true, "parameters", "statements"], 6 | "ban": false, 7 | "class-name": true, 8 | "comment-format": [true, "check-space"], 9 | "curly": false, 10 | "eofline": false, 11 | "forin": true, 12 | "indent": [true, "spaces"], 13 | "interface-name": [true, "never-prefix"], 14 | "jsdoc-format": true, 15 | "jsx-no-lambda": false, 16 | "jsx-no-multiline-js": false, 17 | "label-position": true, 18 | "max-line-length": [false, 120], 19 | "member-ordering": [ 20 | true, 21 | "public-before-private", 22 | "static-before-instance", 23 | "variables-before-functions" 24 | ], 25 | "no-arg": true, 26 | "no-bitwise": true, 27 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 28 | "no-consecutive-blank-lines": true, 29 | "no-construct": true, 30 | "no-debugger": true, 31 | "no-duplicate-variable": true, 32 | "no-empty": true, 33 | "no-eval": true, 34 | "no-string-literal": true, 35 | "no-switch-case-fall-through": true, 36 | "no-trailing-whitespace": false, 37 | "no-unused-expression": false, 38 | "no-use-before-declare": true, 39 | "one-line": [ 40 | true, 41 | "check-catch", 42 | "check-else", 43 | "check-open-brace", 44 | "check-whitespace" 45 | ], 46 | "quotemark": [true, "single", "jsx-double"], 47 | "radix": true, 48 | "semicolon": [false, "always"], 49 | "switch-default": true, 50 | "trailing-comma": false, 51 | "triple-equals": [true, "allow-null-check"], 52 | "typedef": [true, "parameter", "property-declaration"], 53 | "typedef-whitespace": [ 54 | true, 55 | { 56 | "call-signature": "nospace", 57 | "index-signature": "nospace", 58 | "parameter": "nospace", 59 | "property-declaration": "nospace", 60 | "variable-declaration": "nospace" 61 | } 62 | ], 63 | "variable-name": [ 64 | true, 65 | "ban-keywords", 66 | "check-format", 67 | "allow-leading-underscore", 68 | "allow-pascal-case" 69 | ], 70 | "whitespace": [ 71 | true, 72 | "check-branch", 73 | "check-decl", 74 | "check-module", 75 | "check-operator", 76 | "check-separator", 77 | "check-type", 78 | "check-typecast" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/TextInput/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .input { 4 | display: inline-block; 5 | min-width: 200px; 6 | position: relative; 7 | 8 | .clear { 9 | position: absolute; 10 | top: 0; 11 | right: 7px; 12 | bottom: 0; 13 | margin: auto; 14 | font-size: 1.3em; 15 | transition: opacity 0.2s ease-out, visibility 0.2s ease-out; 16 | background-color: darken($text-light, 10%); 17 | cursor: pointer; 18 | mask-image: url('./close.svg'); 19 | height: 0.75em; 20 | width: 0.75em; 21 | z-index: 10; 22 | opacity: 0; 23 | visibility: hidden; 24 | user-select: none; 25 | 26 | &:hover { 27 | background-color: lighten($text-light, 10%); 28 | } 29 | 30 | &:active { 31 | background-color: #785a28; 32 | } 33 | 34 | &.show { 35 | opacity: 1; 36 | visibility: visible; 37 | } 38 | } 39 | 40 | input[type='search'] { 41 | background-image: url('../../assets/search-icon.png'); 42 | background-size: 1.3em; 43 | background-position: 5px center; 44 | background-repeat: no-repeat; 45 | padding-left: 1.8em; 46 | 47 | &:focus { 48 | background: 5px center/1.3em url('../../assets/search-icon.png'), 49 | linear-gradient(to bottom, rgba(7, 16, 25, 0.7), rgba(32, 39, 44, 0.7)); 50 | background-repeat: no-repeat; 51 | } 52 | } 53 | 54 | .inputElement { 55 | width: 100%; 56 | font-family: 'Spiegel'; 57 | font-size: 1em; 58 | padding: 8px 21px; 59 | color: $text-dark; 60 | display: block; 61 | box-sizing: border-box; 62 | border: 1px solid #785a28; 63 | background-color: rgba(0, 0, 0, 0.7); 64 | appearance: none; 65 | outline: none; 66 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 67 | 0 0 0 1px rgba(0, 0, 0, 0.25); 68 | 69 | &:focus { 70 | background: linear-gradient( 71 | to bottom, 72 | rgba(7, 16, 25, 0.7), 73 | rgba(32, 39, 44, 0.7) 74 | ); 75 | border-image: linear-gradient(to bottom, #785a28, #c8aa6e) 1 stretch; 76 | } 77 | 78 | &::-webkit-search-decoration, 79 | &::-webkit-search-cancel-button, 80 | &::-webkit-search-results-button, 81 | &::-webkit-search-results-decoration { 82 | -webkit-appearance: none; 83 | } 84 | } 85 | } 86 | 87 | .input.disabled { 88 | .inputElement, 89 | .inputElement:focus { 90 | background: #1e2328; 91 | border-image: none; 92 | border-color: #3c3c41; 93 | } 94 | 95 | .clear { 96 | background-color: #3c3c41; 97 | cursor: default; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/components/RadioOption/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | $radio-border: linear-gradient( 4 | to bottom, 5 | rgb(62.7%, 48.2%, 18%) 0%, 6 | rgb(43.5%, 32.5%, 14.5%) 80% 7 | ); 8 | $radio-border-hover: linear-gradient( 9 | -45deg, 10 | rgb(78.8%, 61.6%, 24.7%) 0%, 11 | rgb(92.9%, 87.8%, 78%) 80% 12 | ); 13 | $radio-border-active: linear-gradient( 14 | -45deg, 15 | rgb(40.8%, 30.6%, 13.7%) 0%, 16 | rgb(28.6%, 22.4%, 8.2%) 80% 17 | ); 18 | $radio-border-disabled: rgb(23.5%, 21.6%, 19.6%); 19 | 20 | .radioOption { 21 | font-family: 'Spiegel'; 22 | color: $gold-medium; 23 | cursor: pointer; 24 | margin: 0.5rem 0; 25 | outline: none; 26 | 27 | input[type='radio'] { 28 | opacity: 0; 29 | position: absolute; 30 | 31 | &:focus + .label, 32 | & + .label:hover, 33 | .label:focus { 34 | .control { 35 | border-image: $radio-border-hover; 36 | border-image-slice: 1; 37 | } 38 | } 39 | 40 | & + .label:active { 41 | .control { 42 | border-image: $radio-border-active; 43 | border-image-slice: 1; 44 | } 45 | } 46 | } 47 | 48 | .label { 49 | display: flex; 50 | align-items: center; 51 | outline: none; 52 | cursor: pointer; 53 | 54 | .labelText { 55 | font-weight: normal; 56 | letter-spacing: 0.05rem; 57 | margin-left: 0.8rem; 58 | } 59 | 60 | .control { 61 | display: inline-block; 62 | transform: rotateZ(45deg); 63 | backface-visibility: hidden; 64 | width: 0.6rem; 65 | height: 0.6rem; 66 | border: 0.15rem solid transparent; 67 | border-image: $radio-border; 68 | border-image-slice: 1; 69 | fill: transparent; 70 | } 71 | 72 | &:active { 73 | .control { 74 | border-image: $radio-border-active; 75 | border-image-slice: 1; 76 | } 77 | } 78 | } 79 | } 80 | 81 | .radioOption.checked { 82 | .label { 83 | color: $gold; 84 | 85 | .control { 86 | fill: $gold-medium; 87 | } 88 | } 89 | 90 | input[type='radio'] + .label:active { 91 | .control { 92 | fill: url('#hextech-option-active'); 93 | } 94 | } 95 | } 96 | 97 | .radioOption.disabled { 98 | cursor: default; 99 | 100 | .label { 101 | cursor: default; 102 | 103 | .control { 104 | border: 2px solid $radio-border-disabled; 105 | border-image: none; 106 | } 107 | } 108 | 109 | input[type='radio'] { 110 | opacity: 0; 111 | position: absolute; 112 | 113 | &:focus + .label, 114 | & + .label:hover, 115 | .label:focus { 116 | .control { 117 | border-image: none; 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/css/button.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | @import './mixins.scss'; 3 | 4 | :global { 5 | button.hextech { 6 | @include button; 7 | 8 | border: 2px solid #c89c3c; 9 | border-radius: 2px; 10 | border-image: linear-gradient( 11 | to top, 12 | #785b28 0%, 13 | #c89c3c 55%, 14 | #c8a355 71%, 15 | #c8aa6e 100% 16 | ); 17 | border-image-slice: 1; 18 | 19 | &:hover, 20 | &:focus { 21 | backface-visibility: hidden; 22 | border-image: linear-gradient( 23 | to top, 24 | rgb(78.4%, 61.2%, 23.5%) 0%, 25 | rgb(86.3%, 75.7%, 53.3%) 50%, 26 | rgb(88.2%, 78.8%, 59.6%) 71%, 27 | rgb(94.1%, 90.2%, 84.7%) 100% 28 | ); 29 | } 30 | 31 | &:active { 32 | background-image: linear-gradient( 33 | to bottom, 34 | #1e232a 0%, 35 | #1e232a 40%, 36 | rgba(118, 97, 51, 0.8) 140% 37 | ); 38 | color: #5c5b57; 39 | transition: color 100ms linear; 40 | } 41 | } 42 | 43 | button.alternate { 44 | color: #cdfafa; 45 | border-color: #0596aa; 46 | border-image: linear-gradient(to bottom, #0596aa 0, #005a82 100%); 47 | box-shadow: 0 0 25px rgba(0, 0, 0, 0.11); 48 | background-color: #111; 49 | 50 | &:hover, 51 | &:active { 52 | background-image: linear-gradient( 53 | to bottom, 54 | #111 0, 55 | fade-out(lighten(#111, 0.75), 0.1), 56 | 47%, 57 | fade-out(#0596aa, 0.85), 58 | 75%, 59 | fade-out(#005a82, 0.85) 100% 60 | ); 61 | } 62 | 63 | &:active { 64 | color: darken(#cdfafa, 75%); 65 | } 66 | 67 | &.magic { 68 | &::after { 69 | content: ''; 70 | display: block; 71 | background: url('../assets/button-bg-pattern.png') repeat-x top left; 72 | background-size: auto 100%; 73 | background-position: 0 0; 74 | width: 100%; 75 | height: 100%; 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | opacity: 0.3; 80 | animation: buttonBg 500s linear infinite; 81 | animation-play-state: paused; 82 | will-change: background-position; 83 | transition: 0.4s ease; 84 | } 85 | 86 | &:disabled::after { 87 | content: none; 88 | } 89 | } 90 | 91 | &:hover::after { 92 | animation-play-state: running; 93 | opacity: 0.6; 94 | } 95 | } 96 | 97 | button:disabled { 98 | background-color: $gunmetal; 99 | border-image: initial; 100 | border-color: #5c5b57; 101 | } 102 | 103 | @keyframes :global(buttonBg) { 104 | from { 105 | background-position: 0 0; 106 | } 107 | 108 | to { 109 | background-position: 10000px 0; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.m.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | $control-background: $black; 4 | 5 | .control { 6 | user-select: none; 7 | outline: none; 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | 12 | color: $gold-medium; 13 | background: $control-background; 14 | padding: 10px 14px; 15 | border: 1px solid transparent; 16 | 17 | &:not(.transparent) { 18 | border-image: linear-gradient( 19 | to top, 20 | #695625 0%, 21 | #a9852d 23%, 22 | #b88d35 93%, 23 | #c8aa6e 100% 24 | ) 25 | 1; 26 | 27 | &:hover:not(.open), 28 | &:focus:not(.open) { 29 | background: linear-gradient( 30 | to top, 31 | rgba(88, 83, 66, 0.5), 32 | rgba(30, 35, 40, 0.5) 33 | ); 34 | border: 1px solid transparent; 35 | border-image: linear-gradient(to top, #c89b3c, #f0e6d2) 1; 36 | } 37 | 38 | &.open, 39 | &:active { 40 | color: $border-dark; 41 | border: 1px solid $border-dark; 42 | border-image: none; 43 | } 44 | } 45 | 46 | &.transparent { 47 | background: transparent; 48 | border-bottom: none; 49 | transition: all 0.4s ease; 50 | 51 | &:active, 52 | &:focus { 53 | background: $rich-black; 54 | } 55 | 56 | &.open { 57 | border: 1px solid $border-dark; 58 | border-bottom: none; 59 | background: $rich-black; 60 | } 61 | } 62 | } 63 | 64 | .menu { 65 | position: absolute; 66 | top: 100%; 67 | width: 100%; 68 | max-height: 0px; 69 | user-select: none; 70 | overflow-y: hidden; 71 | overflow-x: hidden; 72 | background: $rich-black; 73 | opacity: 0; 74 | border: 1px solid $border-dark; 75 | z-index: 10; 76 | transition: all 0.4s ease; 77 | 78 | &.open { 79 | max-height: 240px; 80 | opacity: 1; 81 | overflow-y: auto; 82 | } 83 | 84 | &:not(.noPlaceholder) { 85 | border-top: none; 86 | } 87 | 88 | .option { 89 | position: relative; 90 | display: flex; 91 | align-items: center; 92 | overflow-x: hidden; 93 | padding: 0 14px; 94 | padding-right: 0; 95 | height: 40px; 96 | border-top: 1px solid $gunmetal; 97 | text-overflow: ellipsis; 98 | white-space: nowrap; 99 | 100 | &.focused { 101 | color: $gold; 102 | background-color: $gunmetal; 103 | } 104 | 105 | &.selected { 106 | padding-right: 1.25em; 107 | padding-right: calc(1.25em + 10px); 108 | background-color: $control-background; 109 | color: $gold; 110 | } 111 | 112 | &:active { 113 | color: $border-dark; 114 | background-color: transparentize($color: $gunmetal, $amount: 0.5); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/SliderInput/index.scss: -------------------------------------------------------------------------------- 1 | $fill: linear-gradient(to left, #695625, #463714); 2 | $fill-hover: linear-gradient(to right, #785a28 0%, #c89b3c 56%, #c8aa6e 100%); 3 | $fill-active: linear-gradient(to right, #695625, #463714); 4 | 5 | .sliderInput { 6 | position: relative; 7 | display: inline-block; 8 | line-height: 0; 9 | width: 100%; 10 | 11 | input[type="range"] { 12 | opacity: 0; 13 | outline: none; 14 | height: 30px; 15 | width: 100%; 16 | /* Ideally this would be 'none' but it breaks IE/Edge 17 | because it doesn't seem to care about pointer events in ::thumb */ 18 | pointer-events: auto; 19 | 20 | &::thumb { 21 | position: relative; 22 | background-color: rgba(255,0,0,0.5); 23 | border-radius: 5px; 24 | height: 30px; 25 | width: 30px; 26 | pointer-events: auto; 27 | } 28 | 29 | &:focus + .control .handle > polygon { 30 | fill: url(#hextech-handle-hover); 31 | } 32 | 33 | &:hover + .control { 34 | & .fill { 35 | background: $fill-hover; 36 | } 37 | 38 | .handle > polygon { 39 | fill: url(#hextech-handle-hover); 40 | } 41 | } 42 | 43 | &:active + .control { 44 | & .fill { 45 | background: $fill-active; 46 | } 47 | 48 | .handle > polygon { 49 | fill: url(#hextech-handle-active); 50 | } 51 | } 52 | 53 | &::track { 54 | border: 0; 55 | width: 100%; 56 | margin-right: -1px; 57 | pointer-events: none; 58 | } 59 | } 60 | 61 | .track { 62 | position: absolute; 63 | top: 0; 64 | bottom: 0; 65 | margin: auto; 66 | background: rgb(30, 35, 40); 67 | height: 2px; 68 | width: 100%; 69 | z-index: 0; 70 | pointer-events: none; 71 | } 72 | 73 | .fill { 74 | position: absolute; 75 | top: 0; 76 | bottom: 0; 77 | margin: auto; 78 | margin-right: 1px; 79 | height: 3px; 80 | background: $fill; 81 | z-index: 1; 82 | pointer-events: none; 83 | } 84 | 85 | .handle { 86 | position: absolute; 87 | top: 0; 88 | bottom: 0; 89 | margin: auto; 90 | height: 30px; 91 | width: 30px; 92 | pointer-events: none; 93 | z-index: 2; 94 | } 95 | 96 | .tooltip { 97 | position: absolute; 98 | opacity: 0; 99 | transition: opacity 0.15s ease-in; 100 | 101 | &.show { 102 | opacity: 1; 103 | transition: opacity 0.3s ease-in 0.15s; 104 | } 105 | } 106 | } 107 | 108 | .sliderInput.disabled { 109 | input[type="range"] { 110 | background: rgb(30, 35, 40); 111 | 112 | & + .control .handle > polygon { 113 | fill: #5c5b57; 114 | } 115 | } 116 | 117 | .fill { 118 | display: none; 119 | } 120 | 121 | .tooltip { 122 | display: none; 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/components/SliderInput/Handle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Handle = props => ( 4 | 12 | 13 | 21 | 22 | 23 | 24 | 32 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 56 | 61 | 62 | 63 | 67 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 86 | 87 | 88 | 96 | 100 | 101 | 102 | ); 103 | 104 | export default Handle; 105 | -------------------------------------------------------------------------------- /src/components/__tests__/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Checkbox from '../Checkbox'; 5 | 6 | describe('', () => { 7 | it('should render correctly', () => { 8 | const checkedWrapper = shallow( 9 | false} className="testClass" value> 10 | Test 1 11 | 12 | ); 13 | const uncheckedWrapper = shallow( 14 | false} className="testClass" value={false}> 15 | Test 2 16 | 17 | ); 18 | 19 | expect(checkedWrapper.get(0)).toMatchSnapshot(); 20 | expect(uncheckedWrapper.get(0)).toMatchSnapshot(); 21 | }); 22 | 23 | it('should appear checked if its value is true', () => { 24 | const wrapper = shallow( 25 | false} value label="test" /> 26 | ); 27 | 28 | expect(wrapper.hasClass('checked')).toBe(true); 29 | }); 30 | 31 | it('should not appear checked if its value is falsey', () => { 32 | const falseWrapper = shallow( 33 | false} value={false} label="test" /> 34 | ); 35 | const nullWrapper = shallow( 36 | false} value={false} label="test" /> 37 | ); 38 | const undefinedWrapper = shallow( 39 | false} value={undefined} label="test" /> 40 | ); 41 | 42 | expect(falseWrapper.hasClass('checked')).toBe(false); 43 | expect(nullWrapper.hasClass('checked')).toBe(false); 44 | expect(undefinedWrapper.hasClass('checked')).toBe(false); 45 | }); 46 | 47 | it('should call onChange with the new value when it is clicked', () => { 48 | const handleChange = jest.fn(); 49 | const wrapper = shallow( 50 | 51 | ); 52 | 53 | handleChange.mockImplementation(v => wrapper.setProps({ value: v })); 54 | 55 | wrapper.simulate('click'); 56 | expect(handleChange).toHaveBeenLastCalledWith(true); 57 | 58 | wrapper.simulate('click'); 59 | expect(handleChange).toHaveBeenLastCalledWith(false); 60 | }); 61 | 62 | it('should call onClick if it defined', () => { 63 | const handleChange = jest.fn(); 64 | const handleClick = jest.fn(); 65 | 66 | const wrapper = shallow( 67 | 73 | ); 74 | 75 | wrapper.simulate('click'); 76 | expect(handleChange).toHaveBeenLastCalledWith(true); 77 | expect(handleClick).toHaveBeenCalled(); 78 | }); 79 | 80 | it('should prefer props.children to props.label', () => { 81 | const label = 'test1'; 82 | const text = 'hello'; 83 | 84 | const wrapper = shallow({text}); 85 | 86 | expect(wrapper.find('.label').text()).toBe(text); 87 | }); 88 | 89 | it('should have a className passed by props', () => { 90 | const className = 'test1'; 91 | 92 | const wrapper = shallow(Hello); 93 | 94 | expect(wrapper.hasClass(className)); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/Switcher/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { withPropsOnChange } from 'recompose'; 3 | 4 | import { Item } from './components'; 5 | 6 | import style from './index.m.scss'; 7 | 8 | export interface SwitcherItem { 9 | key?: string; 10 | label: string; 11 | value?: string; 12 | to?: string; 13 | [key: string]: any; 14 | } 15 | 16 | interface SwitcherProps { 17 | items: SwitcherItem[]; 18 | value?: SwitcherItem; 19 | 20 | onChange: (nextValue: SwitcherItem) => void; 21 | onFocus: React.FocusEventHandler; 22 | onBlur: React.FocusEventHandler; 23 | 24 | getActiveItem: (props: SwitcherProps) => SwitcherItem | void; 25 | } 26 | 27 | class Switcher extends PureComponent { 28 | state = { 29 | x: 0, 30 | width: 0, 31 | renderBar: false, 32 | }; 33 | 34 | list = React.createRef(); 35 | 36 | componentDidMount() { 37 | this.syncBar(); 38 | this.setState({ renderBar: true }); 39 | } 40 | 41 | componentDidUpdate(prevProps: SwitcherProps) { 42 | const { items, value } = this.props; 43 | 44 | if (items !== prevProps.items || value !== prevProps.value) { 45 | this.syncBar(); 46 | } 47 | } 48 | 49 | syncBar() { 50 | const { items, value } = this.props; 51 | const selectedIdx = items.findIndex(item => item === value); 52 | 53 | if (selectedIdx === -1) { 54 | return this.setState({ x: 0, width: 0 }); 55 | } 56 | 57 | const list = this.list.current; 58 | 59 | const selectedElement = list.children[selectedIdx] as HTMLLIElement; 60 | 61 | this.setState({ 62 | x: selectedElement.offsetLeft - list.offsetLeft, 63 | width: selectedElement.clientWidth, 64 | }); 65 | } 66 | 67 | getActiveItem = () => { 68 | const { value, getActiveItem } = this.props; 69 | 70 | if (getActiveItem) { 71 | return getActiveItem(this.props); 72 | } 73 | 74 | return value; 75 | }; 76 | 77 | handleItemClick = item => { 78 | const { onChange } = this.props; 79 | 80 | onChange(item); 81 | }; 82 | 83 | render() { 84 | const { items = [], onFocus, onBlur } = this.props; 85 | const { x, width, renderBar } = this.state; 86 | 87 | const activeItem = this.getActiveItem(); 88 | 89 | return ( 90 |
    91 |
      92 | {items.map(item => ( 93 | 99 | ))} 100 |
    101 | {renderBar && ( 102 |
    106 | )} 107 |
    108 | ); 109 | } 110 | } 111 | 112 | export { Item }; 113 | 114 | export default withPropsOnChange( 115 | ['children', 'items'], 116 | ({ children, items }) => ({ 117 | items: items || React.Children.map(children, child => child.props), 118 | }) 119 | )(Switcher); 120 | -------------------------------------------------------------------------------- /src/components/Dropdown/keyboardEvents.ts: -------------------------------------------------------------------------------- 1 | /* WARNING: Spaghetti */ 2 | 3 | export function handleArrowKeyNavigation(next: number) { 4 | const { options } = this.props; 5 | const { focusedOption } = this.state; 6 | 7 | const currIdx = options.findIndex(o => o === focusedOption); 8 | 9 | const nextIdx = currIdx + next; 10 | 11 | this.navigateToOption(nextIdx); 12 | } 13 | 14 | export function executeStringSearch(tryNext: boolean, start: number = 0) { 15 | clearTimeout(this.searchTimeout); 16 | 17 | const { options } = this.props; 18 | const { focusedOption } = this.state; 19 | 20 | const focusedIdx = options.findIndex(o => o === focusedOption); 21 | 22 | let startIdx = start; 23 | 24 | if (tryNext && focusedIdx > -1 && focusedIdx + 1 < options.length) { 25 | startIdx = focusedIdx + 1; 26 | } 27 | 28 | let nextFocusedIdx = focusedIdx; 29 | 30 | for (let i = startIdx; i < options.length; i += 1) { 31 | const label = options[i].hextech__label; 32 | if (tryNext && label.startsWith(this.searchString[0])) { 33 | nextFocusedIdx = i; 34 | break; 35 | } else if (label.startsWith(this.searchString)) { 36 | nextFocusedIdx = i; 37 | break; 38 | } 39 | } 40 | 41 | if (focusedOption !== options[nextFocusedIdx]) { 42 | this.navigateToOption(nextFocusedIdx); 43 | } else if (tryNext) { 44 | this.navigateToOption( 45 | options.findIndex(o => o.hextech__label.startsWith(this.searchString[0])) 46 | ); 47 | } 48 | 49 | this.searchTimeout = setTimeout(() => { 50 | this.searchString = ''; 51 | }, 1000); 52 | } 53 | 54 | const shouldHandleKey = ({ 55 | keyCode, 56 | ctrlKey, 57 | metaKey, 58 | altKey, 59 | }: KeyboardEvent) => { 60 | if ((keyCode >= 65 && keyCode <= 90) || keyCode === 32) { 61 | if (!ctrlKey && !metaKey && !altKey) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | }; 67 | 68 | export function handleTextSearch(evt: KeyboardEvent) { 69 | if (!shouldHandleKey(evt)) return; 70 | if (evt.keyCode === 32 && this.searchString === '') return; 71 | 72 | evt.preventDefault(); 73 | 74 | const lastTyped = String.fromCharCode(evt.keyCode).toLowerCase(); 75 | const shouldTryNext = 76 | this.searchString[this.searchString.length - 1] === lastTyped; 77 | 78 | this.searchString += lastTyped; 79 | 80 | this.executeStringSearch(shouldTryNext); 81 | } 82 | 83 | export function handleKeyDown(evt: KeyboardEvent) { 84 | if (this.props.disabled) return; 85 | 86 | this.handleTextSearch(evt); 87 | 88 | if (evt.defaultPrevented) { 89 | return; 90 | } 91 | 92 | const { options } = this.props; 93 | 94 | switch (evt.key) { 95 | case 'Tab': 96 | case 'Enter': 97 | return this.handleChange(this.state.focusedOption); 98 | case ' ': 99 | this.handleToggle(true); 100 | break; 101 | case 'Escape': 102 | this.handleToggle(false); 103 | break; 104 | case 'ArrowUp': 105 | case 'ArrowDown': 106 | this.handleArrowKeyNavigation(evt.key === 'ArrowDown' ? 1 : -1); 107 | break; 108 | case 'End': 109 | this.navigateToOption(options.length); 110 | break; 111 | case 'Home': 112 | this.navigateToOption(0); 113 | break; 114 | default: 115 | return; 116 | } 117 | 118 | evt.preventDefault(); 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hextech", 3 | "version": "1.2.5", 4 | "description": "League of Legends Hextect design components", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "webpack -p", 9 | "test": "jest --config jest.json", 10 | "test:coverage": "jest --config jest.json --coverage", 11 | "lint": "eslint --ignore-path .gitignore --format=node_modules/eslint-formatter-pretty src/**/*.js src/**/*.jsx", 12 | "dev": "start-storybook -p 9001 -c .storybook -s .storybook/public", 13 | "eslint-check": "eslint --print-config . | eslint-config-prettier-check", 14 | "export": "build-storybook -c .storybook -o docs" 15 | }, 16 | "files": [ 17 | "lib" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/leaguedevelopers/react-hextech.git" 22 | }, 23 | "keywords": [ 24 | "league", 25 | "of", 26 | "legends", 27 | "lol", 28 | "react", 29 | "hextech", 30 | "webpack", 31 | "javascript" 32 | ], 33 | "author": "S. Coimbra (me@jinx.pro)", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/leaguedevelopers/react-hextech/issues" 37 | }, 38 | "homepage": "https://github.com/leaguedevelopers/react-hextech#readme", 39 | "peerDependencies": { 40 | "react": "^16.0.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.2.2", 44 | "@babel/plugin-proposal-class-properties": "^7.2.3", 45 | "@babel/preset-env": "^7.2.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "@babel/preset-typescript": "^7.1.0", 48 | "@babel/register": "^7.0.0", 49 | "@storybook/addon-a11y": "^4.1.4", 50 | "@storybook/addon-actions": "^4.1.2", 51 | "@storybook/addon-backgrounds": "^4.1.4", 52 | "@storybook/addon-options": "^4.1.2", 53 | "@storybook/react": "^4.1.2", 54 | "@types/classnames": "^2.2.6", 55 | "@types/jest": "^23.3.10", 56 | "@types/react": "^16.7.18", 57 | "@types/styled-components": "^4.1.4", 58 | "autoprefixer": "^6.7.7", 59 | "babel-core": "^6.24.0", 60 | "babel-jest": "^23.6.0", 61 | "babel-loader": "^8.0.4", 62 | "babel-plugin-polished": "^1.1.0", 63 | "babel-plugin-styled-components": "^1.10.0", 64 | "babel-plugin-typescript-to-proptypes": "^0.15.0", 65 | "classnames-loader": "^2.0.0", 66 | "copy-webpack-plugin": "^4.6.0", 67 | "css-loader": "^2.0.1", 68 | "enzyme": "^3.3.0", 69 | "enzyme-adapter-react-16": "^1.1.1", 70 | "enzyme-to-json": "^3.3.4", 71 | "file-loader": "^3.0.1", 72 | "identity-obj-proxy": "^3.0.0", 73 | "jest": "^23.6.0", 74 | "jest-enzyme": "^7.0.1", 75 | "mini-css-extract-plugin": "^0.5.0", 76 | "node-sass": "^4.5.0", 77 | "postcss-input-style": "^0.3.0", 78 | "postcss-loader": "^3.0.0", 79 | "prettier": "^1.15.3", 80 | "prop-types": "^15.6.1", 81 | "raw-loader": "^0.5.1", 82 | "react": "^16.6.3", 83 | "react-dom": "^16.6.3", 84 | "sass-loader": "^6.0.3", 85 | "string-replace-loader": "^2.1.1", 86 | "style-loader": "^0.14.1", 87 | "styled-components": "^4.1.3", 88 | "stylint": "^1.5.9", 89 | "svg-url-loader": "^2.0.2", 90 | "tslint": "^5.12.0", 91 | "tslint-config-prettier": "^1.17.0", 92 | "tslint-react": "^3.6.0", 93 | "typescript": "^3.2.2", 94 | "typescript-plugin-css-modules": "^1.0.4", 95 | "url-loader": "^0.5.8", 96 | "webpack": "^4.27.1", 97 | "webpack-cli": "^3.1.2" 98 | }, 99 | "dependencies": { 100 | "classnames": "^2.2.5", 101 | "element-resize-detector": "^1.1.11", 102 | "lodash.debounce": "^4.0.8", 103 | "lodash.deburr": "^4.1.0", 104 | "polished": "^2.3.1", 105 | "ramda": "^0.26.1", 106 | "recompose": "^0.30.0" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | only: [/theme\.js/], 3 | babelrc: false, 4 | configFile: false, 5 | presets: ['@babel/preset-env'], 6 | }); 7 | 8 | const path = require('path'); 9 | const webpack = require('webpack'); 10 | const ExtractTextPlugin = require('mini-css-extract-plugin'); 11 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 12 | 13 | const replacements = require('./src/theme').replacements; 14 | 15 | const dev = true; 16 | module.exports = { 17 | devtool: 'source-map', 18 | 19 | externals: { 20 | react: { 21 | commonjs: 'react', 22 | commonjs2: 'react', 23 | amd: 'react', 24 | root: 'React', 25 | }, 26 | }, 27 | 28 | entry: ['./src/index.ts'], 29 | 30 | resolve: { 31 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.scss'], 32 | alias: { 33 | '@utils': path.resolve(__dirname, 'src', 'utils'), 34 | '@assets': path.resolve(__dirname, 'src', 'assets'), 35 | '@theme': path.resolve(__dirname, 'src', 'theme'), 36 | '@css': path.resolve(__dirname, 'src', 'css'), 37 | }, 38 | }, 39 | 40 | output: { 41 | path: path.join(__dirname, 'lib'), 42 | filename: 'index.js', 43 | library: 'reactHextech', 44 | libraryTarget: 'umd', 45 | }, 46 | 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.(t|j)sx?$/, 51 | use: [ 52 | 'babel-loader', 53 | { 54 | loader: 'string-replace-loader', 55 | options: { multiple: replacements }, 56 | }, 57 | ], 58 | exclude: /node_modules/, 59 | }, 60 | { 61 | test: /\.(css|scss)$/, 62 | use: [ 63 | { loader: 'classnames-loader' }, 64 | { loader: dev ? 'style-loader' : ExtractTextPlugin.loader }, 65 | { 66 | loader: 'css-loader', 67 | options: { 68 | modules: true, 69 | importLoaders: 2, 70 | localIdentName: 'hextech-[local]-[hash:base64:5]', 71 | }, 72 | }, 73 | { 74 | loader: 'postcss-loader', 75 | options: { 76 | plugins() { 77 | return [ 78 | require('autoprefixer'), // eslint-disable-line 79 | require('postcss-input-style'), // eslint-disable-line 80 | ]; 81 | }, 82 | }, 83 | }, 84 | { loader: 'sass-loader' }, 85 | ], 86 | }, 87 | 88 | { 89 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 90 | loader: 'url-loader?mimetype=application/font-otf', 91 | }, 92 | { 93 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 94 | loader: 'url-loader?mimetype=application/font-woff', 95 | }, 96 | { 97 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 98 | loader: 'url-loader?mimetype=application/font-woff', 99 | }, 100 | { 101 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 102 | loader: 'url-loader?mimetype=application/octet-stream', 103 | }, 104 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 105 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'svg-url-loader' }, 106 | { test: /\.png$/, loader: 'url-loader' }, 107 | { test: /\.jpg$/, loader: 'url-loader' }, 108 | { test: /\.webm/, loader: 'file-loader' }, 109 | { test: /\.ogg/, loader: 'file-loader' }, 110 | ], 111 | }, 112 | 113 | plugins: [ 114 | new webpack.optimize.OccurrenceOrderPlugin(), 115 | 116 | new webpack.DefinePlugin({ 117 | 'process.env.NODE_ENV': JSON.stringify('production'), 118 | }), 119 | 120 | new CopyWebpackPlugin([ 121 | { 122 | from: './src/variables.scss', 123 | to: 'theme.scss', 124 | }, 125 | ]), 126 | 127 | new ExtractTextPlugin({ filename: 'style.css', allChunks: true }), 128 | ], 129 | 130 | target: 'web', 131 | }; 132 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const style = require('./index.scss'); 5 | 6 | interface ButtonProps { 7 | className?: string; 8 | tabIndex?: string | number; 9 | label?: string; 10 | disabled?: boolean; 11 | onClick: React.MouseEventHandler; 12 | } 13 | /** 14 | * Button that looks exactly like the golden border buttons on 15 | * the League Client 16 | */ 17 | export default class Button extends PureComponent { 18 | static defaultProps = { 19 | className: undefined, 20 | tabIndex: '0', 21 | label: undefined, 22 | disabled: false, 23 | children: undefined, 24 | }; 25 | 26 | state = { 27 | isHover: false, 28 | isMouseDown: false, 29 | isClick: false, 30 | }; 31 | 32 | root = React.createRef(); 33 | 34 | /* Click end timeout */ 35 | clickEnd = undefined; 36 | 37 | componentDidMount() { 38 | document.addEventListener('mouseup', this.handleMouseUp as any, false); 39 | } 40 | 41 | componentWillUnmount() { 42 | document.removeEventListener('mouseup', this.handleMouseUp as any, false); 43 | } 44 | 45 | handleClick: React.MouseEventHandler = evt => { 46 | const { onClick, disabled } = this.props; 47 | 48 | if (!disabled) { 49 | this.playClickAnim(); 50 | if (onClick) { 51 | onClick(evt); 52 | } 53 | } 54 | }; 55 | 56 | playClickAnim = () => { 57 | if (this.state.isClick) { 58 | clearTimeout(this.clickEnd); 59 | return this.setState({ isClick: false }, this.playClickAnim); 60 | } 61 | 62 | if (typeof window === 'undefined') { 63 | return; 64 | } 65 | 66 | // Don't block animation if user decides to spam 67 | window.requestAnimationFrame(() => 68 | this.setState({ isClick: true }, () => { 69 | this.clickEnd = setTimeout(() => { 70 | this.setState({ isClick: false }); 71 | }, 300); 72 | }) 73 | ); 74 | }; 75 | 76 | // TODO: might want to turn this into an actual state machine instead of this spaghetti 77 | handleMouseDown = () => this.setState({ isMouseDown: true }); 78 | 79 | handleMouseUp = (evt: React.MouseEvent) => { 80 | const mouseIsOnButton = 81 | this.root.current && this.root.current.contains(evt.target as Node); 82 | this.setState({ isMouseDown: false, isHover: mouseIsOnButton }); 83 | }; 84 | 85 | handleMouseEnter = () => this.setState({ isHover: true }); 86 | 87 | handleMouseLeave = () => this.setState({ isHover: this.state.isMouseDown }); 88 | 89 | render() { 90 | const { className, tabIndex, children, label, disabled } = this.props; 91 | const { isHover, isMouseDown, isClick } = this.state; 92 | const classes = [ 93 | isHover ? style.hover : style.idle, 94 | isMouseDown && style.down, 95 | isClick && style.click, 96 | ]; 97 | 98 | return ( 99 |
    100 |
    115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 |
    {children || label}
    124 |
    125 |
    126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/components/__tests__/RadioInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | 4 | import RadioInput from '../RadioInput'; 5 | 6 | const options = [ 7 | { value: 0, label: 'test0' }, 8 | { value: 1, label: 'test1' }, 9 | { value: 2, label: 'test2' }, 10 | { value: 5, label: 'test3' }, 11 | ]; 12 | 13 | describe.only('', () => { 14 | it('should render correctly', () => { 15 | const props = { 16 | onChange: jest.fn(), 17 | onBlur: jest.fn(), 18 | value: options[1].value, 19 | }; 20 | const wrapper = shallow(); 21 | expect(wrapper.get(0)).toMatchSnapshot(); 22 | }); 23 | 24 | it.only('should render as many children as the options it is provided', () => { 25 | const wrapper = shallow(); 26 | expect(wrapper.children().length).toBe(options.length); 27 | }); 28 | 29 | it('should render no children if no options are provided', () => { 30 | const wrapper = shallow(); 31 | expect(wrapper.children().length).toBe(0); 32 | }); 33 | 34 | it('should render no children if options is undefined or otherwise lacks a map method', () => { 35 | const wrapper = shallow(); 36 | expect(wrapper.children().length).toBe(0); 37 | }); 38 | 39 | it('should render the correct label for each child', () => { 40 | const wrapper = shallow(); 41 | wrapper.children().forEach((child, i) => { 42 | expect(child.props().label).toBe(options[i].label); 43 | }); 44 | }); 45 | 46 | it('should pass the disabled prop to its options', () => { 47 | const wrapper = shallow(); 48 | wrapper.children().forEach(child => { 49 | expect(child.props().disabled).toBe(true); 50 | }); 51 | }); 52 | 53 | it('should mark the selected option, using strict equality against the value prop', () => { 54 | const selectedOption = options[1]; 55 | const value = selectedOption.value; 56 | 57 | const wrapper = shallow(); 58 | const selected = wrapper.childAt(1); 59 | 60 | const unselectedNodes = wrapper 61 | .children() 62 | .map(child => child.props()) 63 | .filter(child => !child.checked); 64 | 65 | expect(selected.props().checked).toBe(true); 66 | expect(unselectedNodes).toHaveLength(options.length - 1); 67 | }); 68 | 69 | it('should call onChange with the new value if the selection changes', () => { 70 | const handleChange = jest.fn(); 71 | const selectedOption = options[1]; 72 | const value = selectedOption.value; 73 | 74 | const wrapper = mount( 75 | 76 | ); 77 | 78 | handleChange.mockImplementation(v => wrapper.setProps({ value: v })); 79 | 80 | const nextSelect = wrapper.childAt(3).find('input'); 81 | 82 | nextSelect.simulate('change'); 83 | expect(handleChange).toHaveBeenCalledWith(options[3].value); 84 | 85 | nextSelect.simulate('change'); 86 | expect(handleChange).toHaveBeenCalledTimes(1); 87 | }); 88 | 89 | it('should not call onChange with the new value if the selection does not change', () => { 90 | const handleChange = jest.fn(); 91 | const selectedOption = options[1]; 92 | const value = selectedOption.value; 93 | 94 | const wrapper = mount( 95 | 96 | ); 97 | 98 | handleChange.mockImplementation(v => wrapper.setProps({ value: v })); 99 | 100 | const nextSelect = wrapper.childAt(1).find('input'); 101 | 102 | nextSelect.simulate('change'); 103 | expect(handleChange).not.toHaveBeenCalled(); 104 | }); 105 | 106 | it('should fail gracefully if neither onClick or onChange are defined', () => { 107 | const selectedOption = options[1]; 108 | const value = selectedOption.value; 109 | 110 | const wrapper = shallow(); 111 | 112 | const nextSelect = wrapper.childAt(3); 113 | 114 | nextSelect.simulate('click'); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/components/Card/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { transparentize } from 'polished'; 3 | 4 | import { t } from '@theme'; 5 | 6 | const overlay = require('@assets/card-overlay.png'); 7 | const flare = require('@assets/card-flare.png'); 8 | 9 | export const Background = styled.div` 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | background-attachment: local; 16 | background-color: #0a0a0c; 17 | background-repeat: no-repeat; 18 | background-size: cover; 19 | width: 100%; 20 | transition: filter 0.5s ease, opacity 0.5s ease; 21 | z-index: 1; 22 | 23 | background-image: url('http://am-a.akamaihd.net/image?f=https%3A%2F%2Funiverse-meeps.leagueoflegends.com%2Fv1%2Fassets%2Fimages%2Faudio-drama-bg.jpg&resize=:810'); 24 | background-position: 0% 0%; 25 | opacity: 1; 26 | `; 27 | 28 | export const Overlay = styled.div` 29 | position: absolute; 30 | bottom: -6px; 31 | left: -6px; 32 | right: -6px; 33 | top: -6px; 34 | background-image: url(${overlay}); 35 | background-position: 0 100%; 36 | background-repeat: no-repeat; 37 | background-size: cover; 38 | z-index: 2; 39 | `; 40 | 41 | export const Flare = styled.div` 42 | position: absolute; 43 | bottom: -6px; 44 | left: -6px; 45 | right: -6px; 46 | top: -6px; 47 | background-image: url(${flare}); 48 | opacity: 0; 49 | transition: opacity 0.4s ease; 50 | background-position: 0 100%; 51 | background-repeat: no-repeat; 52 | background-size: cover; 53 | z-index: 3; 54 | `; 55 | 56 | export const Border = styled.div` 57 | position: absolute; 58 | bottom: -1px; 59 | left: -1px; 60 | right: -1px; 61 | top: -1px; 62 | border-image-slice: 20; 63 | border-image-source: linear-gradient(180deg, #efe5d4 0, #c69b4b); 64 | border-style: solid; 65 | border-width: 2px; 66 | opacity: 0; 67 | transition: opacity 0.4s ease; 68 | z-index: 6; 69 | `; 70 | 71 | export const Labels = styled.div` 72 | position: absolute; 73 | bottom: 66px; 74 | left: 0; 75 | padding: 20px; 76 | padding-bottom: 0; 77 | right: 0; 78 | transition: transform 0.8s ease; 79 | z-index: 3; 80 | 81 | h3, 82 | h4 { 83 | letter-spacing: 1px; 84 | text-align: left; 85 | transition: color 0.4s ease; 86 | font-weight: 700; 87 | } 88 | 89 | h3 { 90 | font-family: 'Beaufort', Arial, sans-serif; 91 | color: #f0e6d2; 92 | font-size: 24px; 93 | line-height: 1.167; 94 | text-transform: uppercase; 95 | width: 100%; 96 | } 97 | 98 | h4 { 99 | font-family: 'Spiegel', Helvetica, sans-serif; 100 | color: #cdbd91; 101 | font-size: 14px; 102 | line-height: 1.143; 103 | padding-bottom: 8px; 104 | text-transform: capitalize; 105 | } 106 | `; 107 | 108 | export const Attributes = styled.div` 109 | display: flex; 110 | align-items: center; 111 | justify-content: space-between; 112 | position: absolute; 113 | bottom: 0; 114 | left: 0; 115 | right: 0; 116 | padding: 20px; 117 | padding-top: 22px; 118 | z-index: 4; 119 | `; 120 | 121 | export const Description = styled.div` 122 | height: 0%; 123 | max-height: 0; 124 | opacity: 0; 125 | transition: opacity 0.25s linear, max-height 1.4s cubic-bezier(0, 0, 0, 1); 126 | 127 | p { 128 | font-family: 'Spiegel', Helvetica, sans-serif; 129 | font-weight: 400; 130 | color: #a09b8c; 131 | font-size: 12px; 132 | line-height: 1.333; 133 | padding: 14px 0 0; 134 | width: 100%; 135 | } 136 | `; 137 | 138 | export const Wrapper = styled.li` 139 | position: relative; 140 | user-select: none; 141 | list-style: none; 142 | cursor: pointer; 143 | overflow: hidden; 144 | /* width: 100%; */ 145 | width: 25%; 146 | height: 370px; 147 | border: 1px solid ${transparentize(0.2, t.gunmetal)}; 148 | box-shadow: 0 0 40px 0 #000; 149 | transition: border-color 0.4s ease; 150 | 151 | &:hover, 152 | &:focus { 153 | ${Border}, ${Flare} { 154 | opacity: 1; 155 | } 156 | 157 | ${Description} { 158 | height: auto; 159 | max-height: 120px; 160 | opacity: 1; 161 | } 162 | } 163 | 164 | &:active { 165 | border-color: ${t.borderDark}; 166 | 167 | ${Border} { 168 | opacity: 0; 169 | } 170 | 171 | ${Labels} h3 { 172 | color: ${t.borderInput}; 173 | } 174 | } 175 | `; 176 | -------------------------------------------------------------------------------- /src/components/SliderInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import cx from 'classnames'; 4 | import elementResizeDetector from 'element-resize-detector'; 5 | 6 | import Frame from '../Frame'; 7 | import Handle from './Handle'; 8 | 9 | const style = require('./index.scss'); 10 | 11 | const erd = elementResizeDetector({ 12 | strategy: 'scroll', 13 | }); 14 | 15 | export default class SliderInput extends PureComponent { 16 | static propTypes = { 17 | // Visual 18 | className: PropTypes.any, 19 | // State 20 | value: PropTypes.number, 21 | min: PropTypes.number, 22 | max: PropTypes.number, 23 | step: PropTypes.number, 24 | disabled: PropTypes.bool, 25 | tooltip: PropTypes.bool, 26 | // Events 27 | onChange: PropTypes.func, 28 | onBlur: PropTypes.func, 29 | }; 30 | 31 | static defaultProps = { 32 | className: undefined, 33 | value: undefined, 34 | min: 0, 35 | max: 100, 36 | step: 10, 37 | disabled: false, 38 | tooltip: false, 39 | onChange: Function.prototype, 40 | onBlur: undefined, 41 | }; 42 | 43 | constructor(props) { 44 | super(props); 45 | 46 | this.state = { 47 | tooltip: false, 48 | width: 0, 49 | }; 50 | } 51 | 52 | componentDidMount() { 53 | // Intentionally discarding first render, don't think there's a better way 54 | erd.listenTo(this.root, ({ offsetWidth }) => 55 | this.handleResize(offsetWidth) 56 | ); 57 | } 58 | 59 | componentWillUnmount() { 60 | erd.removeAllListeners(this.root); 61 | } 62 | 63 | handleChange = evt => { 64 | const { onChange } = this.props; 65 | const nextValue = evt.target.value; 66 | onChange(nextValue); 67 | }; 68 | 69 | handleMouseEnter = () => { 70 | this.props.tooltip && this.setState({ tooltip: true }); 71 | }; 72 | 73 | handleMouseLeave = () => { 74 | this.props.tooltip && this.setState({ tooltip: false }); 75 | }; 76 | 77 | handleResize = width => { 78 | this.setState({ width }); 79 | }; 80 | 81 | render() { 82 | const { 83 | className, 84 | value, 85 | min, 86 | max, 87 | step, 88 | disabled, 89 | tooltip, 90 | onBlur, 91 | } = this.props; 92 | const width = this.state.width; 93 | 94 | let finalValue = parseInt(value, 10); 95 | finalValue = 96 | finalValue >= min && finalValue <= max 97 | ? finalValue 98 | : (min + (max - min)) / 2; 99 | 100 | const fillPercentage = (finalValue / max) * 100; 101 | 102 | const showTooltip = tooltip && this.state.tooltip; 103 | 104 | const left = `${fillPercentage}%`; 105 | let tooltipOffset = left; 106 | // defaults for first render 107 | let handleOffset = `calc(${left} - 15px)`; 108 | let top = -30; 109 | 110 | if (tooltip && width && this.tooltipElem) { 111 | // Find your center... 112 | tooltipOffset = 113 | (width - 30) * (fillPercentage / 100) - 114 | (this.tooltipElem.clientWidth - 30) / 2; 115 | 116 | top = -45 - this.tooltipElem.clientHeight / 2; 117 | } 118 | 119 | if (width) { 120 | handleOffset = (width - 30) * (fillPercentage / 100); 121 | } 122 | 123 | const tooltipStyle = { 124 | top, 125 | left: tooltipOffset, 126 | }; 127 | 128 | const handleStyle = { 129 | left: handleOffset, 130 | }; 131 | 132 | const mouseEvents = { 133 | onMouseEnter: this.handleMouseEnter, 134 | onMouseLeave: this.handleMouseLeave, 135 | }; 136 | 137 | return ( 138 |
    { 140 | this.root = elem; 141 | }} 142 | className={cx(style.sliderInput, disabled && style.disabled, className)} 143 | > 144 | 155 |
    156 |
    0 ? fillPercentage - 1 : 0}%` }} 159 | /> 160 |
    161 | 162 |
    163 | {tooltip && ( 164 |
    { 166 | this.tooltipElem = elem; 167 | }} 168 | style={tooltipStyle} 169 | className={cx(style.tooltip, showTooltip && style.show)} 170 | > 171 | {finalValue} 172 |
    173 | )} 174 |
    175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/components/Button/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | @import '../../css/_mixins.scss'; 3 | 4 | .border { 5 | position: absolute; 6 | top: -2px; 7 | left: -2px; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | .button { 13 | @include button; 14 | 15 | border: 2px solid transparent; 16 | 17 | * { 18 | box-sizing: content-box; 19 | } 20 | 21 | &, 22 | & > div { 23 | cursor: pointer; 24 | user-select: none; 25 | outline: none; 26 | } 27 | 28 | .content { 29 | position: relative; 30 | } 31 | 32 | .buttonBg { 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | width: 100%; 37 | height: 100%; 38 | transition: opacity 400ms cubic-bezier(0, 0, 0.33, 1); 39 | opacity: 0; 40 | backface-visibility: hidden; 41 | background-image: linear-gradient( 42 | to bottom, 43 | #1e232a 0%, 44 | #1e232a 40%, 45 | rgba(118, 97, 51, 0.8) 140% 46 | ); 47 | } 48 | 49 | .borderIdle { 50 | @extend .border; 51 | pointer-events: none; 52 | opacity: 1; 53 | border: 2px solid transparent; 54 | border-image: linear-gradient( 55 | to top, 56 | #785b28 0%, 57 | #c89c3c 55%, 58 | #c8a355 71%, 59 | #c8aa6e 100% 60 | ); 61 | border-image-slice: 1; 62 | transition: opacity 300ms linear; 63 | } 64 | 65 | .borderTransition { 66 | @extend .border; 67 | pointer-events: none; 68 | opacity: 0; 69 | border: 2px solid transparent; 70 | transition: border-color 300ms linear, opacity 300ms linear; 71 | } 72 | 73 | /* SFX */ 74 | .glow { 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | width: 100%; 79 | height: 100%; 80 | filter: blur(4px); 81 | } 82 | 83 | .flare { 84 | &::before { 85 | content: ''; 86 | position: absolute; 87 | top: -25px; 88 | left: -25px; 89 | width: 48px; 90 | height: 48px; 91 | opacity: 0; 92 | background: transparent url('./sheen.png') top center no-repeat; 93 | pointer-events: none; 94 | } 95 | 96 | &::after { 97 | content: ''; 98 | position: absolute; 99 | bottom: -25px; 100 | right: -25px; 101 | width: 48px; 102 | height: 48px; 103 | opacity: 0; 104 | background: transparent url('./sheen.png') top center no-repeat; 105 | pointer-events: none; 106 | } 107 | } 108 | 109 | .sheenWrapper { 110 | position: absolute; 111 | top: 0; 112 | left: 0; 113 | overflow: hidden; 114 | width: 100%; 115 | height: 100%; 116 | pointer-events: none; 117 | 118 | .sheen { 119 | position: absolute; 120 | top: 0; 121 | left: 0; 122 | overflow: hidden; 123 | width: 100%; 124 | height: 150%; 125 | transform: translateY(-100%); 126 | pointer-events: none; 127 | background: linear-gradient( 128 | to bottom, 129 | rgba(255, 255, 255, 0) 0%, 130 | rgba(255, 255, 255, 0.15) 92%, 131 | rgba(255, 255, 255, 0) 100% 132 | ); 133 | filter: blur(2px); 134 | } 135 | } 136 | } 137 | 138 | /* hover state */ 139 | .button.hover { 140 | color: #f0e6d2; 141 | animation: hoverTextShadow 600ms cubic-bezier(0, 0, 0.33, 1) 1; 142 | 143 | .borderTransition { 144 | opacity: 1; 145 | border-image: linear-gradient( 146 | to top, 147 | rgb(78.4%, 61.2%, 23.5%) 0%, 148 | rgb(86.3%, 75.7%, 53.3%) 50%, 149 | rgb(88.2%, 78.8%, 59.6%) 71%, 150 | rgb(94.1%, 90.2%, 84.7%) 100% 151 | ); 152 | border-image-slice: 1; 153 | } 154 | 155 | .glow { 156 | animation: hoverGlow 600ms cubic-bezier(0, 0, 0.33, 1) 1; 157 | } 158 | 159 | .buttonBg { 160 | opacity: 1; 161 | } 162 | } 163 | 164 | /* down state */ 165 | .button.down { 166 | color: #5c5b57; 167 | transition: color 100ms linear; 168 | animation: none; 169 | 170 | .buttonBg { 171 | opacity: 1; 172 | } 173 | } 174 | 175 | /* click state */ 176 | .button.click { 177 | color: #e4e1d8; 178 | border-image: linear-gradient( 179 | to top, 180 | #ffffff 0%, 181 | #ffffff 33%, 182 | #ffffff 66%, 183 | #ffffff 100% 184 | ); 185 | border-image-slice: 1; 186 | animation: clickScale 130ms, hoverTextShadow 400ms; 187 | animation-iteration-count: 1, 1; 188 | animation-timing-function: linear, linear; 189 | 190 | .borderTransition { 191 | border-image: linear-gradient( 192 | to top, 193 | #c89c3c 0%, 194 | #dcc188 50%, 195 | #e1c998 71%, 196 | #f0e6d8 100% 197 | ); 198 | border-image-slice: 1; 199 | transition: opacity 400ms linear; 200 | opacity: 1; 201 | } 202 | 203 | .glow { 204 | animation: hoverGlow 600ms cubic-bezier(0, 0, 0.33, 1) 1; 205 | } 206 | 207 | .sheen { 208 | animation: clickSheen 330ms 1 linear; 209 | } 210 | 211 | .buttonbg { 212 | opacity: 1; 213 | } 214 | 215 | .flare { 216 | &::before { 217 | animation: clickFlare 400ms cubic-bezier(0, 0, 0.33, 1) 0ms 1; 218 | } 219 | 220 | &::after { 221 | animation: clickFlare 400ms cubic-bezier(0, 0, 0.33, 1) 30ms 1; 222 | } 223 | } 224 | } 225 | 226 | .button.disabled { 227 | cursor: default; 228 | color: $text-disabled; 229 | background-color: $gunmetal; 230 | border-image: initial; 231 | 232 | * { 233 | cursor: default; 234 | animation: none; 235 | } 236 | 237 | .flare, 238 | .glow, 239 | .sheenWrapper, 240 | .buttonBg { 241 | display: none; 242 | } 243 | 244 | .borderTransition { 245 | border-color: #5c5b57; 246 | opacity: 1; 247 | } 248 | 249 | .borderIdle { 250 | opacity: 0; 251 | } 252 | } 253 | 254 | @keyframes hoverTextShadow { 255 | 0% { 256 | text-shadow: 0 0 4px #f0e6d8; 257 | } 258 | 259 | 50% { 260 | text-shadow: 0 0 4px rgba(240, 230, 216, 0.5); 261 | } 262 | 263 | 100% { 264 | text-shadow: 0 0 4px rgba(240, 230, 216, 0); 265 | } 266 | } 267 | @keyframes hoverGlow { 268 | 0% { 269 | box-shadow: 0 0 5px 4px rgba(240, 230, 216, 0.5), 270 | 0 0 2px 1px rgba(240, 230, 216, 0.5) inset; 271 | } 272 | 273 | 50% { 274 | box-shadow: 0 0 5px 4px rgba(240, 230, 216, 0.3), 275 | 0 0 2px 1px rgba(240, 230, 216, 0.3) inset; 276 | } 277 | 278 | 100% { 279 | box-shadow: 0 0 5px 4px rgba(240, 230, 216, 0), 280 | 0 0 2px 1px rgba(240, 230, 216, 0) inset; 281 | } 282 | } 283 | @keyframes clickFlare { 284 | 0% { 285 | opacity: 0; 286 | } 287 | 288 | 25% { 289 | opacity: 0.6; 290 | } 291 | 292 | 50% { 293 | opacity: 0.3; 294 | } 295 | 296 | 100% { 297 | opacity: 0; 298 | } 299 | } 300 | @keyframes clickScale { 301 | from { 302 | transform: scale(0.94); 303 | } 304 | 305 | to { 306 | transform: scale(1); 307 | } 308 | } 309 | @keyframes clickSheen { 310 | from { 311 | transform: translateY(-100%) rotate(0deg); 312 | } 313 | 314 | 10% { 315 | transform: translateY(-80%) rotate(-5deg); 316 | } 317 | 318 | to { 319 | transform: translateY(100%) rotate(0deg); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import classnames from 'classnames/bind'; 3 | 4 | import { withOptions } from '@utils'; 5 | 6 | import { Option as OptionType } from '../../utils/option'; 7 | 8 | import Wrapper from './components/Wrapper'; 9 | import Arrow from './components/Arrow'; 10 | 11 | import * as keyboardEventHandlers from './keyboardEvents'; 12 | 13 | import style from './index.m.scss'; 14 | 15 | const cx = classnames.bind(style); 16 | 17 | interface DropdownProps { 18 | className?: any; 19 | placeholder?: string | false; 20 | hideIcon?: boolean; 21 | tabIndex?: number; 22 | disabled?: boolean; 23 | value?: any; 24 | onChange?: (nextValue: OptionType) => void; 25 | onBlur?: React.FocusEventHandler; 26 | onFocus?: React.FocusEventHandler; 27 | onToggle?: (nextOpen: boolean) => void; 28 | options?: OptionType[]; 29 | transparent?: boolean; 30 | isOpen?: boolean; 31 | } 32 | 33 | interface DropdownState { 34 | isOpen: boolean; 35 | focusedOption: any; 36 | focusedIdx: number; 37 | } 38 | /** 39 | * LoL UIKit Dropdown 40 | * Receives options in a array of objects with value and label properties 41 | * value should be present in one of the options' value property 42 | */ 43 | class Dropdown extends PureComponent { 44 | static defaultProps = { 45 | className: undefined, 46 | tabIndex: 0, 47 | disabled: false, 48 | value: undefined, 49 | onChange: Function.prototype, 50 | options: [], 51 | }; 52 | 53 | searchTimeout: number; 54 | searchString = ''; 55 | handleKeyDown: React.KeyboardEventHandler; 56 | 57 | root = React.createRef(); 58 | menu = React.createRef(); 59 | focused = React.createRef(); 60 | 61 | constructor(props: DropdownProps) { 62 | super(props); 63 | 64 | this.state = { 65 | isOpen: false, 66 | focusedOption: props.value, 67 | focusedIdx: -1, 68 | }; 69 | 70 | Object.keys(keyboardEventHandlers).forEach(key => { 71 | this[key] = keyboardEventHandlers[key].bind(this); 72 | }); 73 | } 74 | 75 | componentDidMount() { 76 | if (this.props.isOpen) this.setState({ isOpen: true }); 77 | 78 | document.addEventListener('click', this.handleDocumentClick, false); 79 | document.addEventListener('touchend', this.handleDocumentClick, false); 80 | } 81 | 82 | componentWillUnmount() { 83 | clearTimeout(this.searchTimeout); 84 | document.removeEventListener('click', this.handleDocumentClick, false); 85 | document.removeEventListener('touchend', this.handleDocumentClick, false); 86 | } 87 | 88 | componentDidUpdate(_: any, prevState: DropdownState) { 89 | const { isOpen, focusedOption } = this.state; 90 | const { menu, focused } = this; // DOM refs 91 | 92 | if (!focused.current) { 93 | return; 94 | } 95 | 96 | // only update if necessary 97 | if (focusedOption === prevState.focusedOption) { 98 | if (isOpen === prevState.isOpen) { 99 | return; 100 | } 101 | } 102 | 103 | if (isOpen && menu.current.scrollTop >= focused.current.offsetTop) { 104 | menu.current.scrollTop = focused.current.offsetTop; 105 | return; 106 | } 107 | 108 | if ( 109 | isOpen && 110 | focused.current.getBoundingClientRect().bottom > 111 | menu.current.getBoundingClientRect().bottom 112 | ) { 113 | menu.current.scrollTop = 114 | focused.current.offsetTop + 115 | focused.current.clientHeight - 116 | menu.current.clientHeight; 117 | } 118 | } 119 | 120 | handleChange = nextOption => { 121 | const { onChange, value } = this.props; 122 | 123 | this.handleOptionFocus(nextOption); 124 | 125 | if (nextOption !== value) { 126 | onChange(nextOption); 127 | } 128 | 129 | this.handleToggle(false); 130 | }; 131 | 132 | handleDocumentClick = evt => { 133 | if (this.state.isOpen && !this.root.current.contains(evt.target)) { 134 | this.handleToggle(false); 135 | } 136 | }; 137 | 138 | // Prevent dropdown from losing focus before option is selected 139 | handleOptionMouseDown = (evt: React.MouseEvent) => { 140 | evt.preventDefault(); 141 | }; 142 | 143 | handleToggle = forceNextOpen => { 144 | const { onToggle, disabled } = this.props; 145 | const { isOpen } = this.state; 146 | 147 | if (disabled) return; 148 | 149 | const nextOpen = forceNextOpen.target ? !isOpen : forceNextOpen; 150 | 151 | if (nextOpen === this.state.isOpen) { 152 | return; 153 | } 154 | 155 | this.setState({ 156 | isOpen: nextOpen, 157 | }); 158 | 159 | if (onToggle) onToggle(nextOpen); 160 | }; 161 | 162 | handleOptionFocus = option => { 163 | this.setState({ focusedOption: option }); 164 | }; 165 | 166 | navigateToOption = index => { 167 | const { options } = this.props; 168 | const { isOpen } = this.state; 169 | 170 | if (typeof index !== 'number') return; 171 | 172 | let nextIdx = index; 173 | if (nextIdx < 0) { 174 | nextIdx = options.length - 1; 175 | } else if (nextIdx >= options.length) { 176 | nextIdx = 0; 177 | } 178 | 179 | if (!isOpen) { 180 | this.handleChange(options[nextIdx]); 181 | } else if (nextIdx !== index) { 182 | return; // if the select is open, don't loop over 183 | } 184 | this.handleOptionFocus(options[nextIdx]); 185 | }; 186 | 187 | renderOption = option => { 188 | const key = option.key || option.label; 189 | const selected = this.props.value === option; 190 | const focused = this.state.focusedOption === option; 191 | 192 | return ( 193 |
    this.handleChange(option)} 201 | onMouseEnter={() => this.handleOptionFocus(option)} 202 | > 203 | {option.label} 204 |
    205 | ); 206 | }; 207 | 208 | render() { 209 | const { 210 | options, 211 | className, 212 | tabIndex, 213 | value, 214 | disabled, 215 | transparent, 216 | onFocus, 217 | onBlur, 218 | placeholder = 'Select...', 219 | hideIcon, 220 | } = this.props; 221 | 222 | const { isOpen } = this.state; 223 | const open = disabled ? false : !!isOpen; 224 | 225 | const noPlaceholder = placeholder === false; 226 | 227 | return ( 228 | 235 | {!noPlaceholder && ( 236 |

    241 | {value ? value.label : placeholder} 242 | {!hideIcon && } 243 |

    244 | )} 245 |
    251 | {options.map(this.renderOption)} 252 |
    253 |
    254 | ); 255 | } 256 | } 257 | 258 | export default withOptions(Dropdown); 259 | -------------------------------------------------------------------------------- /.storybook/index.css: -------------------------------------------------------------------------------- 1 | :global { 2 | html, 3 | body { 4 | height: 100%; 5 | } 6 | 7 | #root { 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | #rune-builder { 16 | height: 90%; 17 | margin: 0 auto; 18 | } 19 | 20 | .dropdown-wrapper { 21 | min-width: 350px; 22 | } 23 | 24 | .slider-wrapper { 25 | width: 350px; 26 | } 27 | 28 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 29 | 30 | /** 31 | * 1. Change the default font family in all browsers (opinionated). 32 | * 2. Correct the line height in all browsers. 33 | * 3. Prevent adjustments of font size after orientation changes in 34 | * IE on Windows Phone and in iOS. 35 | */ 36 | 37 | /* Document 38 | ========================================================================== */ 39 | 40 | html { 41 | font-family: sans-serif; /* 1 */ 42 | line-height: 1.15; /* 2 */ 43 | -ms-text-size-adjust: 100%; /* 3 */ 44 | -webkit-text-size-adjust: 100%; /* 3 */ 45 | } 46 | 47 | /* Sections 48 | ========================================================================== */ 49 | 50 | /** 51 | * Remove the margin in all browsers (opinionated). 52 | */ 53 | 54 | body { 55 | margin: 0; 56 | } 57 | 58 | /** 59 | * Add the correct display in IE 9-. 60 | */ 61 | 62 | article, 63 | aside, 64 | footer, 65 | header, 66 | nav, 67 | section { 68 | display: block; 69 | } 70 | 71 | /** 72 | * Correct the font size and margin on `h1` elements within `section` and 73 | * `article` contexts in Chrome, Firefox, and Safari. 74 | */ 75 | 76 | h1 { 77 | font-size: 2em; 78 | margin: 0.67em 0; 79 | } 80 | 81 | /* Grouping content 82 | ========================================================================== */ 83 | 84 | /** 85 | * Add the correct display in IE 9-. 86 | * 1. Add the correct display in IE. 87 | */ 88 | 89 | figcaption, 90 | figure, 91 | main { 92 | /* 1 */ 93 | display: block; 94 | } 95 | 96 | /** 97 | * Add the correct margin in IE 8. 98 | */ 99 | 100 | figure { 101 | margin: 1em 40px; 102 | } 103 | 104 | /** 105 | * 1. Add the correct box sizing in Firefox. 106 | * 2. Show the overflow in Edge and IE. 107 | */ 108 | 109 | hr { 110 | box-sizing: content-box; /* 1 */ 111 | height: 0; /* 1 */ 112 | overflow: visible; /* 2 */ 113 | } 114 | 115 | /** 116 | * 1. Correct the inheritance and scaling of font size in all browsers. 117 | * 2. Correct the odd `em` font sizing in all browsers. 118 | */ 119 | 120 | pre { 121 | font-family: monospace, monospace; /* 1 */ 122 | font-size: 1em; /* 2 */ 123 | } 124 | 125 | /* Text-level semantics 126 | ========================================================================== */ 127 | 128 | /** 129 | * 1. Remove the gray background on active links in IE 10. 130 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 131 | */ 132 | 133 | a { 134 | background-color: transparent; /* 1 */ 135 | -webkit-text-decoration-skip: objects; /* 2 */ 136 | } 137 | 138 | /** 139 | * Remove the outline on focused links when they are also active or hovered 140 | * in all browsers (opinionated). 141 | */ 142 | 143 | a:active, 144 | a:hover { 145 | outline-width: 0; 146 | } 147 | 148 | /** 149 | * 1. Remove the bottom border in Firefox 39-. 150 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 151 | */ 152 | 153 | abbr[title] { 154 | border-bottom: none; /* 1 */ 155 | text-decoration: underline; /* 2 */ 156 | text-decoration: underline dotted; /* 2 */ 157 | } 158 | 159 | /** 160 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 161 | */ 162 | 163 | b, 164 | strong { 165 | font-weight: inherit; 166 | } 167 | 168 | /** 169 | * Add the correct font weight in Chrome, Edge, and Safari. 170 | */ 171 | 172 | b, 173 | strong { 174 | font-weight: bolder; 175 | } 176 | 177 | /** 178 | * 1. Correct the inheritance and scaling of font size in all browsers. 179 | * 2. Correct the odd `em` font sizing in all browsers. 180 | */ 181 | 182 | code, 183 | kbd, 184 | samp { 185 | font-family: monospace, monospace; /* 1 */ 186 | font-size: 1em; /* 2 */ 187 | } 188 | 189 | /** 190 | * Add the correct font style in Android 4.3-. 191 | */ 192 | 193 | dfn { 194 | font-style: italic; 195 | } 196 | 197 | /** 198 | * Add the correct background and color in IE 9-. 199 | */ 200 | 201 | mark { 202 | background-color: #ff0; 203 | color: #000; 204 | } 205 | 206 | /** 207 | * Add the correct font size in all browsers. 208 | */ 209 | 210 | small { 211 | font-size: 80%; 212 | } 213 | 214 | /** 215 | * Prevent `sub` and `sup` elements from affecting the line height in 216 | * all browsers. 217 | */ 218 | 219 | sub, 220 | sup { 221 | font-size: 75%; 222 | line-height: 0; 223 | position: relative; 224 | vertical-align: baseline; 225 | } 226 | 227 | sub { 228 | bottom: -0.25em; 229 | } 230 | 231 | sup { 232 | top: -0.5em; 233 | } 234 | 235 | /* Embedded content 236 | ========================================================================== */ 237 | 238 | /** 239 | * Add the correct display in IE 9-. 240 | */ 241 | 242 | audio, 243 | video { 244 | display: inline-block; 245 | } 246 | 247 | /** 248 | * Add the correct display in iOS 4-7. 249 | */ 250 | 251 | audio:not([controls]) { 252 | display: none; 253 | height: 0; 254 | } 255 | 256 | /** 257 | * Remove the border on images inside links in IE 10-. 258 | */ 259 | 260 | img { 261 | border-style: none; 262 | } 263 | 264 | /** 265 | * Hide the overflow in IE. 266 | */ 267 | 268 | svg:not(:root) { 269 | overflow: hidden; 270 | } 271 | 272 | /* Forms 273 | ========================================================================== */ 274 | 275 | /** 276 | * 1. Change the font styles in all browsers (opinionated). 277 | * 2. Remove the margin in Firefox and Safari. 278 | */ 279 | 280 | button, 281 | input, 282 | optgroup, 283 | select, 284 | textarea { 285 | font-family: sans-serif; /* 1 */ 286 | font-size: 100%; /* 1 */ 287 | line-height: 1.15; /* 1 */ 288 | margin: 0; /* 2 */ 289 | } 290 | 291 | /** 292 | * Show the overflow in IE. 293 | * 1. Show the overflow in Edge. 294 | */ 295 | 296 | button, 297 | input { 298 | /* 1 */ 299 | overflow: visible; 300 | } 301 | 302 | /** 303 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 304 | * 1. Remove the inheritance of text transform in Firefox. 305 | */ 306 | 307 | button, 308 | select { 309 | /* 1 */ 310 | text-transform: none; 311 | } 312 | 313 | /** 314 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 315 | * controls in Android 4. 316 | * 2. Correct the inability to style clickable types in iOS and Safari. 317 | */ 318 | 319 | button, 320 | html [type="button"], /* 1 */ 321 | [type="reset"], 322 | [type="submit"] { 323 | -webkit-appearance: button; /* 2 */ 324 | } 325 | 326 | /** 327 | * Remove the inner border and padding in Firefox. 328 | */ 329 | 330 | button::-moz-focus-inner, 331 | [type='button']::-moz-focus-inner, 332 | [type='reset']::-moz-focus-inner, 333 | [type='submit']::-moz-focus-inner { 334 | border-style: none; 335 | padding: 0; 336 | } 337 | 338 | /** 339 | * Restore the focus styles unset by the previous rule. 340 | */ 341 | 342 | button:-moz-focusring, 343 | [type='button']:-moz-focusring, 344 | [type='reset']:-moz-focusring, 345 | [type='submit']:-moz-focusring { 346 | outline: 1px dotted ButtonText; 347 | } 348 | 349 | /** 350 | * Change the border, margin, and padding in all browsers (opinionated). 351 | */ 352 | 353 | fieldset { 354 | border: 1px solid #c0c0c0; 355 | margin: 0 2px; 356 | padding: 0.35em 0.625em 0.75em; 357 | } 358 | 359 | /** 360 | * 1. Correct the text wrapping in Edge and IE. 361 | * 2. Correct the color inheritance from `fieldset` elements in IE. 362 | * 3. Remove the padding so developers are not caught out when they zero out 363 | * `fieldset` elements in all browsers. 364 | */ 365 | 366 | legend { 367 | box-sizing: border-box; /* 1 */ 368 | color: inherit; /* 2 */ 369 | display: table; /* 1 */ 370 | max-width: 100%; /* 1 */ 371 | padding: 0; /* 3 */ 372 | white-space: normal; /* 1 */ 373 | } 374 | 375 | /** 376 | * 1. Add the correct display in IE 9-. 377 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 378 | */ 379 | 380 | progress { 381 | display: inline-block; /* 1 */ 382 | vertical-align: baseline; /* 2 */ 383 | } 384 | 385 | /** 386 | * Remove the default vertical scrollbar in IE. 387 | */ 388 | 389 | textarea { 390 | overflow: auto; 391 | } 392 | 393 | /** 394 | * 1. Add the correct box sizing in IE 10-. 395 | * 2. Remove the padding in IE 10-. 396 | */ 397 | 398 | [type='checkbox'], 399 | [type='radio'] { 400 | box-sizing: border-box; /* 1 */ 401 | padding: 0; /* 2 */ 402 | } 403 | 404 | /** 405 | * Correct the cursor style of increment and decrement buttons in Chrome. 406 | */ 407 | 408 | [type='number']::-webkit-inner-spin-button, 409 | [type='number']::-webkit-outer-spin-button { 410 | height: auto; 411 | } 412 | 413 | /** 414 | * 1. Correct the odd appearance in Chrome and Safari. 415 | * 2. Correct the outline style in Safari. 416 | */ 417 | 418 | [type='search'] { 419 | -webkit-appearance: textfield; /* 1 */ 420 | outline-offset: -2px; /* 2 */ 421 | } 422 | 423 | /** 424 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 425 | */ 426 | 427 | [type='search']::-webkit-search-cancel-button, 428 | [type='search']::-webkit-search-decoration { 429 | -webkit-appearance: none; 430 | } 431 | 432 | /** 433 | * 1. Correct the inability to style clickable types in iOS and Safari. 434 | * 2. Change font properties to `inherit` in Safari. 435 | */ 436 | 437 | ::-webkit-file-upload-button { 438 | -webkit-appearance: button; /* 1 */ 439 | font: inherit; /* 2 */ 440 | } 441 | 442 | /* Interactive 443 | ========================================================================== */ 444 | 445 | /* 446 | * Add the correct display in IE 9-. 447 | * 1. Add the correct display in Edge, IE, and Firefox. 448 | */ 449 | 450 | details, /* 1 */ 451 | menu { 452 | display: block; 453 | } 454 | 455 | /* 456 | * Add the correct display in all browsers. 457 | */ 458 | 459 | summary { 460 | display: list-item; 461 | } 462 | 463 | /* Scripting 464 | ========================================================================== */ 465 | 466 | /** 467 | * Add the correct display in IE 9-. 468 | */ 469 | 470 | canvas { 471 | display: inline-block; 472 | } 473 | 474 | /** 475 | * Add the correct display in IE. 476 | */ 477 | 478 | template { 479 | display: none; 480 | } 481 | 482 | /* Hidden 483 | ========================================================================== */ 484 | 485 | /** 486 | * Add the correct display in IE 10-. 487 | */ 488 | 489 | [hidden] { 490 | display: none; 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | @mixin focusStyles { 4 | .box { 5 | .border { 6 | backface-visibility: hidden; 7 | border-color: transparent; 8 | border-image: linear-gradient( 9 | to bottom, 10 | $border-dark 50%, 11 | lighten($border-dark, 15%) 100% 12 | ); 13 | border-image-slice: 1; 14 | } 15 | } 16 | 17 | .label { 18 | color: lighten($text-dark, 20%); 19 | } 20 | } 21 | 22 | .checkbox { 23 | &, 24 | * { 25 | cursor: pointer; 26 | } 27 | 28 | input { 29 | display: none; 30 | } 31 | 32 | .visible { 33 | outline: none; 34 | } 35 | 36 | .box { 37 | position: relative; 38 | vertical-align: middle; 39 | display: inline-block; 40 | height: 1em; 41 | width: 1em; 42 | min-width: 12px; 43 | min-height: 12px; 44 | 45 | .border { 46 | position: absolute; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | border: 0.15em solid transparent; 52 | border-color: $border-dark; 53 | } 54 | 55 | .check { 56 | position: absolute; 57 | bottom: 0; 58 | right: 0; 59 | left: 0; 60 | display: none; 61 | color: lighten(rgb(56.1%, 45.1%, 20%), 10%); 62 | font-size: 1em; 63 | } 64 | } 65 | 66 | .label { 67 | color: $text-dark; 68 | font-weight: normal; 69 | letter-spacing: 0.025em; 70 | margin-left: 9px; 71 | user-select: none; 72 | vertical-align: middle; 73 | } 74 | 75 | &:hover, 76 | input:focus + .visible { 77 | @include focusStyles(); 78 | } 79 | } 80 | 81 | .checkbox.checked { 82 | .box .check { 83 | display: block; 84 | } 85 | 86 | &:hover { 87 | .check { 88 | color: $gold; 89 | } 90 | } 91 | } 92 | 93 | .checkbox.disabled { 94 | * { 95 | cursor: default; 96 | } 97 | 98 | &:hover, 99 | input:focus + .visible { 100 | .box { 101 | .border { 102 | border-image: none; 103 | } 104 | } 105 | } 106 | 107 | .box { 108 | .border { 109 | border-color: $text-disabled; 110 | } 111 | } 112 | 113 | .label { 114 | color: $text-disabled; 115 | } 116 | } 117 | 118 | /* Embedded Checkmark Font */ 119 | @font-face { 120 | font-family: 'check-mark'; 121 | src: url('data:application/octet-stream;base64,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') 122 | format('woff'), 123 | url('data:application/octet-stream;base64,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') 124 | format('truetype'); 125 | } 126 | 127 | .iconCheck:before { 128 | content: '\e801'; 129 | font-family: 'check-mark'; 130 | font-style: normal; 131 | font-weight: normal; 132 | display: inline-block; 133 | text-decoration: inherit; 134 | text-align: center; 135 | font-variant: normal; 136 | text-transform: none; 137 | margin-left: 0.2em; 138 | } 139 | --------------------------------------------------------------------------------