diff --git a/.eslintrc b/.eslintrc index 043e6ba2..5b38b7a8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -227,7 +227,7 @@ "react/no-danger": 0, "react/no-did-mount-set-state": 0, "react/no-did-update-set-state": 1, - "react/no-multi-comp": 0, + "react/no-multi-comp": 1, "react/no-unknown-property": 1, "react/prop-types": [2, {"ignore": ["onMouseDown", "onTouchStart"]}], "react/react-in-jsx-scope": 1, diff --git a/components/avatar/Avatar.jsx b/components/avatar/Avatar.jsx new file mode 100644 index 00000000..503d60e7 --- /dev/null +++ b/components/avatar/Avatar.jsx @@ -0,0 +1,67 @@ +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import FontIcon from '../font_icon'; // ewww! :P @TODO +import style from './style'; + +@ClassNames(style) +class Avatar extends Component { + + static propTypes = { + accent: PropTypes.bool, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]), + className: PropTypes.string, + classNames: PropTypes.func, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.element + ]), + image: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.element + ]), + primary: PropTypes.bool, + size: PropTypes.number.required + } + + static defaultProps = { + size: 40 + } + + render () { + const { + accent, + children, + className, + classNames, + icon, + primary, + image, + ...otherProps + } = this.props; + + let component; + + const classes = classNames('avatar', { + accent, + primary + }, className); + + if (typeof image === 'string') { + component = ; + } else if (typeof image === 'string') { + component = ; + } else if (typeof icon === 'string') { + component = ; + } + + return ( +
{component ? component : children}
+ ); + } + +} + +export default Avatar; diff --git a/components/avatar/index.js b/components/avatar/index.js new file mode 100644 index 00000000..2c1f2bf4 --- /dev/null +++ b/components/avatar/index.js @@ -0,0 +1 @@ +export Avatar from './Avatar.jsx'; diff --git a/components/avatar/style.scss b/components/avatar/style.scss new file mode 100644 index 00000000..3f849d1b --- /dev/null +++ b/components/avatar/style.scss @@ -0,0 +1,11 @@ +.avatar { + width: 40px; + height: 40px; + overflow: hidden; + border-radius: 50%; +} + +.avatarImg { + max-width: 100%; + height: auto; +} diff --git a/components/card/Card.jsx b/components/card/Card.jsx index c06c439d..210f7382 100644 --- a/components/card/Card.jsx +++ b/components/card/Card.jsx @@ -1,87 +1,36 @@ -import React from 'react'; -import Navigation from '../navigation'; -import Ripple from '../ripple'; -import style from './style'; +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import styles from './style'; + +@ClassNames(styles) +class Card extends Component { -class Card extends React.Component { static propTypes = { - actions: React.PropTypes.array, - className: React.PropTypes.string, - color: React.PropTypes.string, - image: React.PropTypes.string, - loading: React.PropTypes.bool, - onClick: React.PropTypes.func, - subtitle: React.PropTypes.string, - text: React.PropTypes.string, - title: React.PropTypes.string, - type: React.PropTypes.oneOf(['wide', 'event', 'image']) - }; - - static defaultProps = { - className: '', - loading: false - }; - - handleMouseDown = (event) => { - if (this.props.onClick) { - event.preventDefault(); - this.refs.ripple.start(event); - this.props.onClick(event, this); - } - }; - - renderActions () { - if (this.props.actions) { - return ( - - ); - } - } - - renderTitle () { - const styleFigure = {}; - const styleOverflow = {}; - if (this.props.image) styleFigure.backgroundImage = `url(${this.props.image})`; - if (this.props.color) { - styleFigure.backgroundColor = this.props.color; - styleOverflow.backgroundColor = this.props.color; - } - - if (this.props.title || this.props.image) { - return ( -
- {this.props.title ?
{this.props.title}
: null} - {this.props.subtitle ? {this.props.subtitle} : null} - {this.props.color ?
: null} -
- ); - } + children: PropTypes.any, + className: PropTypes.string, + classNames: PropTypes.func, + raised: PropTypes.bool } render () { - let className = style.root; - if (this.props.type) className += ` ${style[this.props.type]}`; - if (this.props.onClick) className += ` ${style.touch}`; - if (this.props.image || this.props.color) className += ` ${style.contrast}`; - if (this.props.color) className += ` ${style.color}`; - if (this.props.loading) className += ` ${style.loading}`; - if (this.props.className) className += ` ${this.props.className}`; + const { + children, + className, + classNames, + raised, + ...otherProps + } = this.props; + + const classes = classNames('card', { + 'raised': raised + }, className); return (
- {this.renderTitle()} - {this.props.text ?

{this.props.text}

: null} - {this.renderActions()} - + {children}
); } diff --git a/components/card/CardActions.jsx b/components/card/CardActions.jsx new file mode 100644 index 00000000..605d40aa --- /dev/null +++ b/components/card/CardActions.jsx @@ -0,0 +1,32 @@ +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import style from './style'; + +@ClassNames(style) +class CardActions extends Component { + + static propTypes = { + children: PropTypes.any, + className: PropTypes.string, + classNames: PropTypes.func + } + + render () { + const { + children, + className, + classNames, + ...otherProps + } = this.props; + + const classes = classNames('cardActions', className); + + return ( +
+ {children} +
+ ); + } +} + +export default CardActions; diff --git a/components/card/CardMedia.jsx b/components/card/CardMedia.jsx new file mode 100644 index 00000000..64faca5d --- /dev/null +++ b/components/card/CardMedia.jsx @@ -0,0 +1,48 @@ +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import style from './style'; + +@ClassNames(style) +class CardMedia extends Component { + + static propTypes = { + aspectRatio: PropTypes.oneOf([ 'wide', 'square' ]), + children: PropTypes.node, + className: PropTypes.string, + classNames: PropTypes.func, + color: PropTypes.string, + image: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]) + } + + render () { + const { + aspectRatio, + children, + className, + classNames, + color, + image, + ...otherProps + } = this.props; + + const classes = classNames('cardMedia', aspectRatio, className); + + const bgStyle = { + backgroundColor: color ? color : undefined, + backgroundImage: typeof image === 'string' ? `url('${image}')` : undefined + }; + + return ( +
+
+ {children} +
+
+ ); + } +} + +export default CardMedia; diff --git a/components/card/CardText.jsx b/components/card/CardText.jsx new file mode 100644 index 00000000..e2014bca --- /dev/null +++ b/components/card/CardText.jsx @@ -0,0 +1,32 @@ +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import style from './style'; + +@ClassNames(style) +class CardText extends Component { + + static propTypes = { + children: PropTypes.any, + className: PropTypes.string, + classNames: PropTypes.func + } + + render () { + const { + children, + className, + classNames, + ...otherProps + } = this.props; + + const classes = classNames('cardText', className); + + return ( +
+ {typeof children === 'string' ?

{children}

: children} +
+ ); + } +} + +export default CardText; diff --git a/components/card/CardTitle.jsx b/components/card/CardTitle.jsx new file mode 100644 index 00000000..56e8a64a --- /dev/null +++ b/components/card/CardTitle.jsx @@ -0,0 +1,61 @@ +import React, { PropTypes, Component } from 'react'; +import ClassNames from '../decorators/ClassNames'; +import { Avatar } from '../avatar'; +import style from './style'; + +@ClassNames(style) +class CardTitle extends Component { + + static propTypes = { + avatar: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.element + ]), + children: PropTypes.string, + className: PropTypes.string, + classNames: PropTypes.func, + subtitle: PropTypes.string, + title: PropTypes.string + } + + render () { + let avatarComponent; + + const { + avatar, + children, + className, + classNames, + subtitle, + title, + ...otherProps + } = this.props; + + const classes = classNames('cardTitle', { + 'small': avatar, + 'large': !avatar + }, className); + + if (typeof avatar === 'string') { + avatarComponent = ; + } else { + avatarComponent = avatar; + } + + return ( +
+ {avatarComponent && ( +
+ {avatarComponent} +
+ )} +
+ {(title || children) &&
{title ? title : children}
} + {subtitle &&

{subtitle}

} +
+
+ ); + } +} + +export default CardTitle; diff --git a/components/card/_config.scss b/components/card/_config.scss index 05ace434..1f85e577 100644 --- a/components/card/_config.scss +++ b/components/card/_config.scss @@ -1,7 +1,10 @@ -$card-color-white: $color-white !default; -$card-title-height: 17.6 * $unit; -$card-width-normal: 32 * $unit; -$card-width-large: 51.2 * $unit; -$card-offset: 1.6 * $unit; -$card-navigation-offset: $card-offset / 2; -$card-text-overlay: rgba($color-black, 0.2); +$card-color-white: unquote("rgb(#{$color-white})") !default; +$card-text-overlay: unquote("rgba(#{$color-black}, 0.2)"); + +$card-background-color: $card-color-white; + +$card-padding-sm: 8px; +$card-padding: 16px; +$card-padding-lg: 20px; + +$card-font-size: $font-size-small; diff --git a/components/card/index.js b/components/card/index.js index e69de29b..4935ce94 100644 --- a/components/card/index.js +++ b/components/card/index.js @@ -0,0 +1,5 @@ +export Card from './Card.jsx'; +export CardActions from './CardActions.jsx'; +export CardMedia from './CardMedia.jsx'; +export CardText from './CardText.jsx'; +export CardTitle from './CardTitle.jsx'; diff --git a/components/card/index.jsx b/components/card/old.jsx similarity index 100% rename from components/card/index.jsx rename to components/card/old.jsx diff --git a/components/card/style.scss b/components/card/style.scss index 48dd8809..c255ab47 100644 --- a/components/card/style.scss +++ b/components/card/style.scss @@ -1,112 +1,122 @@ @import "../base"; @import "./config"; -.figure { - position: relative; +.card { + // Box Model display: flex; - min-height: $card-title-height; flex-direction: column; - justify-content: flex-end; - background-position: center center; - background-size: cover; - > *:not(.overflow) { - z-index: $z-index-normal; - font-weight: $font-weight-normal; + border-radius: 2px; + overflow: hidden; + + // Fonts + font-size: $card-font-size; + + // Colors + background: $card-background-color; + + // Elevation + @include shadow-2dp(); + + // Raised Elevation as per spec + &.raised { + @include shadow-8dp(); } - > .overflow { +} + +.cardMedia { + position: relative; + background-repeat: no-repeat; + background-size: cover; + background-position: center center; + height: 0; + + &.wide { + width: 100%; + padding-top: 56.25%; + } + + &.square { + width: 100%; + padding-top: 100%; + } + + .content { + // Positioning position: absolute; top: 0; left: 0; - z-index: 0; + + // Box Model + display: flex; + flex-direction: column; width: 100%; height: 100%; - opacity: .75; } } -.text { - font-size: $font-size-small; - line-height: $font-size-big; - color: $color-text-secondary; -} - -.navigation { - padding: $card-navigation-offset; - > * { - min-width: 0; - padding-right: $card-navigation-offset; - padding-left: $card-navigation-offset; - } -} - -.ripple { - background-color: $color-text-secondary; -} - -.root { - @include shadow-2dp(); - position: relative; +.cardTitle { display: flex; - width: $card-width-normal; - flex-direction: column; - overflow: hidden; - vertical-align: top; - background: $color-background; - > *:not(.navigation) { - padding: $card-offset; - } - &:not(.color) > *:not(.figure), > *:not(:last-child) { - box-shadow: 0 1px $color-divider; - } -} + align-items: center; -.touch { - cursor: pointer; -} + .avatar { + margin-right: 13px; + } -.contrast { - .figure { - color: $card-color-white; - text-shadow: 0; + .subtitle { + color: $color-text-secondary; } - .ripple { - background-color: $card-color-white; - } -} -.loading { - pointer-events: none; - cursor: none; - filter: grayscale(100%); - .ripple { - @include ripple-loading(cardloading, 2 * $card-width-normal, 2 * $card-width-normal); - width: 2 * $card-width-normal; - height: 2 * $card-width-normal; - animation-name: cardloading; - } -} + &.large { + padding: $card-padding-lg $card-padding ($card-padding - 2px); -.image { - &, .figure { - height: $card-width-normal; + .title { + @include typo-headline(); + line-height: 1.25; + } } - .figure { - padding: 0; - > h5 { - padding: $card-offset; - font-size: $font-size-small; - font-weight: $font-weight-bold; - background-color: $card-text-overlay; + + &.small { + padding: $card-padding; + + .title { + @include typo-body-2(false, true); + line-height: 1.4; + } + .subtitle { + font-weight: 500; + line-height: 1.4; } } } -.event { - .figure { - justify-content: flex-start; +.cardTitle, .cardText { + padding: ($card-padding - 2px) $card-padding; + + &:last-child { + padding-bottom: $card-padding-lg; + } + + & + .cardText { + padding-top: 0; } } -.wide { - width: $card-width-large; +.cardActions { + display: flex; + align-items: center; + justify-content: flex-start; + padding: $card-padding-sm $card-padding-sm; + + button[data-react-toolbox="button"] { + min-width: 0; + margin: 0 $card-padding-sm/2; + + &:first-child { + margin-left: $card-padding-sm; + } + + &:last-child { + margin-right: $card-padding-sm; + } + } } diff --git a/components/decorators/ClassNames.jsx b/components/decorators/ClassNames.jsx new file mode 100644 index 00000000..cb0cb585 --- /dev/null +++ b/components/decorators/ClassNames.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ClassNames from 'classnames/bind'; + +/** + * Provides a component with a classNames + * function bound to the provided CSS module + * + * Check for 'classNames()' in props + * + * @param {CSS Module} styles + * @return {Component} + */ +function classNames (styles) { + const classNamesFn = ClassNames.bind(styles); + + return function classNamesDecorator (ComposedComponent) { + + return function ClassNamesDecor (props) { + + return ( + + ); + }; + }; +} + + +export default classNames; diff --git a/spec/components/card.jsx b/spec/components/card.jsx index 0f4dde4b..34bb9e93 100644 --- a/spec/components/card.jsx +++ b/spec/components/card.jsx @@ -1,37 +1,85 @@ import React from 'react'; -import Card from '../../components/card'; +import Button from '../../components/button'; +import { + Card, + CardActions, + CardMedia, + CardText, + CardTitle +} from '../../components/card'; +import style from '../style'; + +const dummyText = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.'; + +const demos = [ + { + name: 'Basic Card', + component: ( + + Basic Card + {dummyText} + +