├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── components ├── _base.scss ├── _mixins.scss ├── _shapes.scss ├── avatar │ ├── avatar.jsx │ ├── gravatar.js │ ├── index.js │ └── style.scss ├── channel │ ├── addchannel.jsx │ ├── channel.jsx │ ├── index.js │ ├── list.jsx │ └── style.scss ├── common │ ├── contextMenuStyle.scss │ ├── contextmenu.jsx │ ├── index.js │ ├── input.jsx │ ├── menuitem.jsx │ ├── navSectionStyle.scss │ ├── navsection.jsx │ ├── spinner.jsx │ └── style.scss ├── index.js ├── markdown │ ├── help.jsx │ ├── index.js │ └── style.scss ├── message │ ├── action.jsx │ ├── age.jsx │ ├── base.scss │ ├── counter.jsx │ ├── counters.scss │ ├── github.scss │ ├── index.js │ ├── message.jsx │ ├── messageinput.jsx │ ├── replyform.scss │ ├── style.scss │ ├── uploadbutton.jsx │ └── username.jsx ├── thread │ ├── contributors.jsx │ ├── day.jsx │ ├── index.js │ ├── list.jsx │ ├── style.scss │ ├── thread.jsx │ ├── threadform.jsx │ └── util.js ├── user │ ├── index.js │ ├── list.jsx │ ├── menu.jsx │ ├── menuStyle.scss │ └── style.scss └── util.js ├── demo ├── actions.js ├── api.js ├── app.jsx ├── devtools.jsx ├── index.jsx ├── state.js ├── store.js └── style.scss ├── deploy.sh ├── devserver.js ├── fiber.iml ├── index.html ├── lib ├── _base.scss ├── _mixins.scss ├── _shapes.scss ├── avatar │ ├── avatar.js │ ├── gravatar.js │ ├── index.js │ └── style.scss ├── channel │ ├── addchannel.js │ ├── channel.js │ ├── index.js │ ├── list.js │ └── style.scss ├── common │ ├── contextMenuStyle.scss │ ├── contextmenu.js │ ├── index.js │ ├── input.js │ ├── menuitem.js │ ├── navSectionStyle.scss │ ├── navsection.js │ ├── spinner.js │ └── style.scss ├── index.js ├── markdown │ ├── help.js │ ├── index.js │ └── style.scss ├── message │ ├── action.js │ ├── age.js │ ├── base.scss │ ├── counter.js │ ├── counters.scss │ ├── github.scss │ ├── index.js │ ├── message.js │ ├── messageinput.js │ ├── replyform.scss │ ├── style.scss │ ├── uploadbutton.js │ └── username.js ├── thread │ ├── contributors.js │ ├── day.js │ ├── index.js │ ├── list.js │ ├── style.scss │ ├── thread.js │ ├── threadform.js │ └── util.js ├── user │ ├── index.js │ ├── list.js │ ├── menu.js │ ├── menuStyle.scss │ └── style.scss └── util.js ├── lint.cmd ├── make.cmd ├── makesite.sh ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = crlf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | build 3 | lib 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true, 9 | "es6": true 10 | }, 11 | 12 | "globals": { 13 | "$": true, 14 | "swal": true, 15 | }, 16 | 17 | "rules": { 18 | "id-length": 0, 19 | "indent": ["error", 2, {"SwitchCase": 1}], 20 | "no-console": 0, 21 | "no-plusplus": 0, 22 | "func-names": 0, 23 | "no-use-before-define": 0, 24 | "linebreak-style": 0, 25 | "import/no-extraneous-dependencies": 0, 26 | "react/no-multi-comp": 0, 27 | "react/prop-types": 0, 28 | "react/jsx-indent": ["error", 2], 29 | "jsx-a11y/no-static-element-interactions": 0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.1.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.1.0 4 | env: 5 | global: 6 | - GH_REF: github.com/reactbits/fiber.git 7 | - secure: FB4oP9AX77IBVhPRTwSqoDmUX6xE1uk8nACcgRvM4oZXVBEh+dV4a4wDltg1Zli+Unbkl4U9pSKwLjAyQkiHntutmUvOxk2F1ZUq/qL7M4JbBpA93Xzb7+ecaXviomkQ5E7kXqvmgoLMI9TtSnf6oOnJ7RCUfu04pxVO+gpvcvOAk8V/hRKyJvNSS6FBKfUHZobDj93pxF51Jnr5JOH5nrpP3nYz/iaSj5lgT7+yeESYwXCfzgF9VPEmnLZykq4kOAIhwpm3yvxLM3zpt51TUaDZKAoljjcJEoE73BLwkmTthffL4pbRQ0odLIIGai5cUnsB2mDHBnwMYpyvO2tSwZcJhAXKp4yLL4r9oz99Zx5mSS+7PJ/EZ+inJ29DpcO03pfGoctIcdBWrwGTAwiHUbKKZselBQ1Vbugnn93vViOl+eLfeb9ERI4U8nsDe7ZacuZ0vEfwjaQ5LI+KDcnmsYhs3KvQu5COK82juj3sZbNbhSQPGGioIWAQcxaM69onIUK+TO9yz3J21qeHN4GD7GE0rYhakVyh2X2hJPdQwwVcwuzk38raMXsLnLZ6/Wj/5v82GKWgjWyO/wI0bALV5Apjc6f6zEDP+K69Tel+uKsP3baaiIUSOatyBiJY2rTjYfATYPmsv0mTIqShsfAIY+nOhu0sMZ28H56s2uPWNew= 8 | script: 9 | - npm test 10 | - bash ./deploy.sh 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 reactbits 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/react-fiber.svg)](https://badge.fury.io/js/react-fiber) 2 | [![Build Status](https://travis-ci.org/reactbits/fiber.svg?branch=master)](https://travis-ci.org/reactbits/fiber) 3 | [![Total downloads](https://img.shields.io/npm/dt/react-fiber.svg)](https://www.npmjs.com/package/react-fiber) 4 | 5 | [![Dependency Status](https://david-dm.org/reactbits/fiber.svg)](https://david-dm.org/reactbits/fiber) 6 | [![devDependency Status](https://david-dm.org/reactbits/fiber/dev-status.svg)](https://david-dm.org/reactbits/fiber#info=devDependencies) 7 | 8 | # fiber 9 | 10 | React components to use in messaging applications. 11 | -------------------------------------------------------------------------------- /components/_base.scss: -------------------------------------------------------------------------------- 1 | @import "./_mixins"; 2 | 3 | $lightgreen: #42aa42; 4 | $lightblue: #1fadc5; 5 | 6 | $input-focus-color: $lightblue; 7 | 8 | .input { 9 | border: 1px solid rgba(0,0,0,0.1); 10 | border-radius: .2em; 11 | outline: none; 12 | } 13 | 14 | .input:focus { 15 | box-shadow: 0 0 .2em $input-focus-color inset; 16 | border-color: $input-focus-color; 17 | } 18 | -------------------------------------------------------------------------------- /components/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin border-radius ($radius: 4px) { 2 | -webkit-border-radius: $radius; 3 | -moz-border-radius: $radius; 4 | -ms-border-radius: $radius; 5 | -o-border-radius: $radius; 6 | border-radius: $radius; 7 | } 8 | 9 | @mixin transition ($args...) { 10 | -webkit-transition: $args; 11 | -moz-transition: $args; 12 | -o-transition: $args; 13 | -ms-transition: $args; 14 | transition: $args; 15 | } 16 | 17 | @mixin transform-transition ($args...) { 18 | -webkit-transition: -webkit-transform $args; 19 | -moz-transition: -moz-transform $args; 20 | -o-transition: -o-transform $args; 21 | -ms-transition: -ms-transform $args; 22 | transition: transform $args; 23 | } 24 | 25 | @mixin transform ($args...) { 26 | -webkit-transform: $args; 27 | -moz-transform: $args; 28 | -o-transform: $args; 29 | -ms-transform: $args; 30 | transform: $args; 31 | } 32 | 33 | @mixin arrow-left ($position: 50%, $width: 7px, $length: 10px, $background-color: #fcfcfc, $border-color: #ddd) { 34 | &:before { 35 | content: ''; 36 | position: absolute; 37 | border-style: solid; 38 | border-width: $width $length $width 0; 39 | border-color: transparent $border-color; 40 | display: block; 41 | width: 0; 42 | z-index: 0; 43 | margin-top: -$width; 44 | left: (-$length - 1); 45 | top: $position; 46 | } 47 | &:after { 48 | content: ''; 49 | position: absolute; 50 | border-style: solid; 51 | border-width: $width $length $width 0; 52 | border-color: transparent $background-color; 53 | display: block; 54 | width: 0; 55 | z-index: 1; 56 | margin-top: -$width; 57 | left: -$length; 58 | top: $position; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /components/_shapes.scss: -------------------------------------------------------------------------------- 1 | @import "./_mixins"; 2 | 3 | @mixin heart($size:24px, $color:red) { 4 | position: relative; 5 | width: $size; 6 | height: $size*0.9; 7 | &:before, 8 | &:after { 9 | position: absolute; 10 | content: ""; 11 | left: $size/2; 12 | top: 0; 13 | width: $size/2; 14 | height: $size*0.8; 15 | background: $color; 16 | border-radius: $size/2 $size/2 0 0; 17 | transform: rotate(-45deg); 18 | transform-origin: 0 100%; 19 | } 20 | &:after { 21 | left: 0; 22 | transform: rotate(45deg); 23 | transform-origin :100% 100%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /components/avatar/avatar.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component, PropTypes } from 'react'; 3 | import classNames from 'classnames'; 4 | import ImageLoader from 'react-imageloader'; 5 | import { hover, hint } from 'css-effects'; 6 | import is from 'is_js'; 7 | import { Spinner } from '../common'; 8 | import style from './style.scss'; 9 | import gravatarURL from './gravatar'; 10 | import { toPromise } from '../util'; 11 | 12 | const avatarSizes = { 13 | small: 24, 14 | sm: 24, 15 | s: 24, 16 | normal: 32, 17 | medium: 32, 18 | m: 32, 19 | large: 40, 20 | l: 40, 21 | xl: 64, 22 | }; 23 | 24 | const defaultSize = 32; 25 | 26 | export function avatarSize(value) { 27 | return (_.isString(value) ? avatarSizes[value.toLowerCase()] : value) || defaultSize; 28 | } 29 | 30 | function avatarURL(url) { 31 | if (is.email(url)) { 32 | return gravatarURL(url); 33 | } 34 | return url; 35 | } 36 | 37 | function makePreloader(size) { 38 | return () => { 39 | const css = { 40 | width: size, 41 | height: size, 42 | }; 43 | return ( 44 |
45 | 46 |
47 | ); 48 | }; 49 | } 50 | 51 | function RandomAvatar(props) { 52 | // TODO render random avatar 53 | const size = props.size; 54 | return ( 55 | avatar 56 | ); 57 | } 58 | 59 | function makeWrapper(props) { 60 | return (sourceProps, content) => { 61 | const attrs = { ...sourceProps, ...props }; 62 | if (props.title) { 63 | delete attrs.title; 64 | attrs.className = classNames(attrs.className, hint()); 65 | attrs['data-hint'] = props.title; 66 | } 67 | return ( 68 |
{content}
69 | ); 70 | }; 71 | } 72 | 73 | export default class Avatar extends Component { 74 | static propTypes = { 75 | className: PropTypes.string, 76 | size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 77 | }; 78 | 79 | static defaultProps = { 80 | className: '', 81 | size: 'normal', 82 | style: {}, 83 | }; 84 | 85 | constructor(props) { 86 | super(props); 87 | 88 | let source = props.source; 89 | if (!source) { 90 | const user = props.user; 91 | if (user && _.isObject(user)) { 92 | source = user.avatar_url || user.avatar; 93 | } 94 | } 95 | 96 | this.state = { source, user: props.user }; 97 | } 98 | 99 | online() { 100 | if (this.props.online) { 101 | return this.props.online; 102 | } 103 | const user = this.state.user; 104 | return user && _.isObject(user) && user.online; 105 | } 106 | 107 | title() { 108 | const value = this.props.title || this.props.name; 109 | if (value) return value; 110 | const user = this.state.user; 111 | if (user && _.isObject(user)) { 112 | return user.name || user.login; 113 | } 114 | return ''; 115 | } 116 | 117 | render() { 118 | // TODO shadow 119 | const props = this.props; 120 | const shape = style[props.shape || 'circle']; 121 | const hoverEffect = hover(props.hover); 122 | const className = classNames( 123 | this.props.className, 124 | style.avatar, { 125 | [shape]: true, 126 | [style.online]: this.online(), 127 | [style.circled]: props.circled, 128 | [hoverEffect]: true, 129 | }, 130 | ); 131 | 132 | const src = avatarURL(this.state.source); 133 | const size = avatarSize(this.props.size); 134 | const avatarStyle = { 135 | width: size + 8, 136 | height: size + 8, 137 | marginLeft: -(size + 8), 138 | ...this.props.style, 139 | }; 140 | 141 | const imgProps = { 142 | width: size, 143 | height: size, 144 | }; 145 | 146 | const wrapper = makeWrapper({ 147 | title: this.title(), 148 | }); 149 | 150 | const preloader = makePreloader(size); 151 | 152 | function empty() { 153 | return ( 154 |
155 | {preloader()} 156 |
157 | ); 158 | } 159 | 160 | const sourcePromise = toPromise(src); 161 | if (sourcePromise) { 162 | sourcePromise.then((value) => { 163 | // TODO fix possible call of setState after unmount 164 | this.setState({ source: value }); 165 | }); 166 | return empty(); 167 | } 168 | 169 | if (!src && this.props.user) { 170 | const userPromise = toPromise(this.props.user); 171 | if (userPromise) { 172 | userPromise.then((user) => { 173 | // TODO fix possible call of setState after unmount 174 | this.setState({ 175 | user, 176 | source: user.avatar_url || user.avatar, 177 | }); 178 | }); 179 | return empty(); 180 | } 181 | } 182 | 183 | if (!_.isString(src)) { 184 | // TODO render error 185 | return empty(); 186 | } 187 | 188 | const loaderProps = { 189 | src, 190 | wrapper, 191 | preloader, 192 | imgProps, 193 | }; 194 | 195 | return ( 196 | 197 | 198 | 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /components/avatar/gravatar.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import queryString from 'query-string'; 3 | 4 | const defaultOptions = { 5 | d: 'retro', 6 | }; 7 | 8 | function isHash(s) { 9 | return /^[a-f0-9]{32}$/i.test((s || '').trim().toLowerCase()); 10 | } 11 | 12 | // TODO cash gravatar URLs 13 | export default function gravatarURL(email, size = 32, options = defaultOptions) { 14 | let url = 'https://secure.gravatar.com/avatar/'; 15 | 16 | if (isHash(email)) { 17 | url += email; 18 | } else { 19 | url += md5(email.toLowerCase()); 20 | } 21 | 22 | const qs = queryString.stringify({ s: size, ...options }); 23 | if (qs) { 24 | url += `?${qs}`; 25 | } 26 | 27 | return url; 28 | } 29 | -------------------------------------------------------------------------------- /components/avatar/index.js: -------------------------------------------------------------------------------- 1 | export { default, avatarSize } from './avatar'; 2 | -------------------------------------------------------------------------------- /components/avatar/style.scss: -------------------------------------------------------------------------------- 1 | @import "../_base"; 2 | @import "../_mixins"; 3 | 4 | .preloader { 5 | justify-content: center; 6 | } 7 | 8 | .avatar { 9 | cursor: pointer; 10 | position: relative; 11 | margin: 4px 4px 4px -45px; 12 | overflow: visible; 13 | } 14 | 15 | .avatar.circle img { 16 | border-radius: 50%; 17 | } 18 | 19 | .avatar.round_rect img { 20 | border: 1px solid transparent; 21 | border-radius: 4px; 22 | } 23 | 24 | .avatar.round_rect img:hover { 25 | border: 1px solid transparentize(#1fadc5, 0.5); 26 | } 27 | 28 | .avatar.circled { 29 | border: 2px solid lighten(orange, 25%); 30 | border-radius: 50%; 31 | padding: 2px; 32 | } 33 | 34 | .online:after { 35 | content: "\f400"; 36 | color: #4cbe00; 37 | position: absolute; 38 | top: -.4em; 39 | right: -.4em; 40 | text-shadow: 0 0 .1em #fff; 41 | font-size: .8em; 42 | font-family: "Ionicons"; 43 | z-index: 99; 44 | } 45 | 46 | @mixin shadow ($size: 2px) { 47 | border: $size solid #fff; 48 | box-shadow: 0 1px 1px rgba(0,0,0,0.3); 49 | } 50 | 51 | .shadow { 52 | @include shadow() 53 | } 54 | .shadow_1 { 55 | @include shadow(1px) 56 | } 57 | .shadow_2 { 58 | @include shadow(2px) 59 | } 60 | .shadow_3 { 61 | @include shadow(3px) 62 | } 63 | -------------------------------------------------------------------------------- /components/channel/addchannel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Flex, Box } from 'reflexbox'; 4 | import { Modal, Button } from 'react-bootstrap'; 5 | import { Form, Input } from 'reactbits-input'; 6 | import style from './style.scss'; 7 | 8 | class Dialog extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | show: true, 13 | }; 14 | } 15 | 16 | render() { 17 | const close = () => this.setState({ show: false }); 18 | const inputs = { 19 | name: { 20 | name: 'name', 21 | placeholder: 'Channel name', 22 | required: true, 23 | }, 24 | description: { 25 | name: 'description', 26 | placeholder: 'Description', 27 | }, 28 | }; 29 | return ( 30 | 31 |
32 | 33 | Create new channel 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default function newChannelDialog(callback) { 54 | let wrapper = null; 55 | const submit = (data) => { 56 | setTimeout(() => { 57 | ReactDOM.unmountComponentAtNode(wrapper); 58 | wrapper.remove(); 59 | callback(data); 60 | }, 100); 61 | }; 62 | wrapper = document.body.appendChild(document.createElement('div')); 63 | ReactDOM.render(, wrapper); 64 | } 65 | -------------------------------------------------------------------------------- /components/channel/channel.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import style from './style.scss'; 5 | import { NavItem } from '../common'; 6 | 7 | function removeButton(onClick) { 8 | return ×; 9 | } 10 | 11 | // TODO render channel actions 12 | export default function Channel(props) { 13 | const canRemove = _.isFunction(props.remove); 14 | const className = classNames(style.channel, { 15 | [style.selected_channel]: props.selected, 16 | }); 17 | 18 | const select = () => { 19 | if (_.isFunction(props.select)) { 20 | props.select(props.data); 21 | } 22 | }; 23 | 24 | const itemProps = { 25 | className, 26 | onClick: select, 27 | selected: props.selected, 28 | to: props.to, 29 | }; 30 | 31 | return ( 32 | 33 | {props.data.name} 34 | 35 | {canRemove ? removeButton(props.remove) : null} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/channel/index.js: -------------------------------------------------------------------------------- 1 | export { default as Channel } from './channel'; 2 | export { default as ChannelList } from './list'; 3 | -------------------------------------------------------------------------------- /components/channel/list.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import Channel from './channel'; 5 | import newChannelDialog from './addchannel'; 6 | import style from './style.scss'; 7 | import { NavSection, NavBody, NavHeader, NavHeaderButtons, PlusButton } from '../common'; 8 | 9 | export default function ChannelList(props) { 10 | const className = classNames(props.className, style.channel_list); 11 | const selectedId = (props.selectedChannel || {}).id; 12 | const channels = _.map(props.channels, (cn, i) => { 13 | const cnprops = { 14 | key: cn.id || i, 15 | data: cn, 16 | selected: cn.id === selectedId, 17 | select: props.selectChannel, 18 | remove: _.isFunction(props.removeChannel) ? props.removeChannel.bind(null, cn) : undefined, 19 | to: cn.id && props.basePath ? `${props.basePath}/${cn.id}` : undefined, 20 | }; 21 | return ; 22 | }); 23 | const onPlusClick = () => { 24 | newChannelDialog(props.createChannel); 25 | }; 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {channels} 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/channel/style.scss: -------------------------------------------------------------------------------- 1 | @import "../_base"; 2 | 3 | .btn_add_channel { 4 | font-size: 20px; 5 | } 6 | 7 | .btn_remove_channel { 8 | margin-left: 4px; 9 | font-size: 20px; 10 | font-weight: bold; 11 | cursor: pointer; 12 | } 13 | -------------------------------------------------------------------------------- /components/common/contextMenuStyle.scss: -------------------------------------------------------------------------------- 1 | @import "../_base"; 2 | 3 | .context_menu .Popover-body { 4 | display: inline-flex; 5 | flex-direction: column; 6 | background: white; 7 | border-radius: 0.3rem; 8 | box-shadow: 0 0 10px rgba(0,0,0,0.25); 9 | } 10 | 11 | .context_menu .Popover-tipShape { 12 | fill: white; 13 | } 14 | 15 | .context_menu ul { 16 | list-style-type: none; 17 | margin: .75rem 0; 18 | padding: 0; 19 | } 20 | 21 | .context_menu ul li { 22 | line-height: 1.5rem; 23 | margin: 0; 24 | } 25 | 26 | .context_menu ul li a { 27 | cursor: pointer; 28 | text-decoration: none; 29 | padding: 0 1rem 0 .75rem; 30 | margin: 0 .75rem; 31 | border-radius: .25rem; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | white-space: nowrap; 35 | outline: 0; 36 | font-size: 15px; 37 | line-height: 25px; 38 | background: 0 0; 39 | color: #2c2d30; 40 | } 41 | 42 | .context_menu ul li a:hover { 43 | background-color: rgb(0, 156, 255); 44 | color: #fff; 45 | } 46 | 47 | .context_menu .header_item { 48 | cursor: default; 49 | margin: .625rem .75rem; 50 | position: relative; 51 | } 52 | 53 | .context_menu .header_item hr { 54 | border: 0; 55 | border-top: 1px solid #ddd; 56 | border-bottom: 1px solid #fff; 57 | position: absolute; 58 | left: 1rem; 59 | right: 0; 60 | margin: 1.25rem 0; 61 | margin-top: .6rem; 62 | margin-bottom: 0; 63 | } 64 | 65 | .context_menu .header_item .header_label { 66 | position: relative; 67 | background-color: #fff; 68 | color: #9e9ea6; 69 | padding-right: .8rem; 70 | } 71 | 72 | .show_button { 73 | cursor: pointer; 74 | text-decoration: none; 75 | } 76 | 77 | .show_button:hover { 78 | text-decoration: none; 79 | } 80 | -------------------------------------------------------------------------------- /components/common/contextmenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import PopoverClass from 'react-popover'; 4 | import styles from './contextMenuStyle.scss'; 5 | 6 | const popover = React.createFactory(PopoverClass); 7 | 8 | export default class ContextMenu extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | dropdownVisible: false, 13 | }; 14 | } 15 | 16 | render() { 17 | const { button } = this.props; 18 | 19 | const hide = () => this.setState({ dropdownVisible: false }); 20 | const showDropdown = (e) => { 21 | e.preventDefault(); 22 | e.stopPropagation(); 23 | this.setState({ dropdownVisible: true }); 24 | return false; 25 | }; 26 | 27 | const buttonProps = { 28 | className: classNames(styles.show_button, button.className), 29 | onMouseDown: showDropdown, 30 | }; 31 | const buttonElement = {button.content}; 32 | 33 | const dropdownProps = { 34 | className: styles.context_menu, 35 | isOpen: this.state.dropdownVisible, 36 | preferPlace: 'below', 37 | place: 'below', 38 | onOuterAction: hide, 39 | body: , 40 | refreshIntervalMs: false, 41 | }; 42 | 43 | return ( 44 | 45 | {popover(dropdownProps, buttonElement)} 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /components/common/index.js: -------------------------------------------------------------------------------- 1 | export { default as Input } from './input'; 2 | export { default as Spinner } from './spinner'; 3 | export { default as ContextMenu } from './contextmenu'; 4 | export { default as ContextMenuItem } from './menuitem'; 5 | export { 6 | NavSection, 7 | NavHeader, 8 | NavHeaderButtons, 9 | NavBody, 10 | NavItem, 11 | IconButton, 12 | PlusButton, 13 | } from './navsection'; 14 | -------------------------------------------------------------------------------- /components/common/input.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { findDOMNode } from 'react-dom'; 4 | import style from './style.scss'; 5 | 6 | export default class Input extends Component { 7 | componentDidMount() { 8 | if (this.props.focused) { 9 | this.focus(); 10 | } 11 | } 12 | 13 | componentWillReceiveProps(nextProps) { 14 | if (nextProps.focused) { 15 | this.focus(); 16 | } 17 | } 18 | 19 | input = null; 20 | 21 | focus() { 22 | if (this.input) { 23 | const node = findDOMNode(this.input); 24 | if (node) { 25 | node.focus(); 26 | } 27 | } 28 | } 29 | 30 | render() { 31 | // eslint-disable-next-line no-unused-vars 32 | const { cancel, submit, focused, ...props } = this.props || {}; 33 | 34 | const onKeyUp = (event) => { 35 | if (event.which === 27) { 36 | const input = $(event.target); 37 | input.blur(); 38 | if (_.isFunction(props.cancel)) { 39 | cancel(); 40 | return; 41 | } 42 | return; 43 | } 44 | if (event.ctrlKey && event.which === 13 && _.isFunction(submit)) { 45 | submit(); 46 | } 47 | }; 48 | 49 | const onMouseDown = () => { 50 | this.focus(); 51 | }; 52 | 53 | const attrs = { 54 | ref: (c) => { this.input = c; }, 55 | className: props.className || style.input, 56 | type: 'text', 57 | onKeyUp, 58 | onMouseDown, 59 | ...props, 60 | }; 61 | 62 | return