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 (
-
- );
- }
+ 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 (
+
+ );
+ }
+}
+
+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}
+
+
+
+
+
+ )
+ }, {
+ name: '16:9 Card Media',
+ component: (
+
+
+
+ {dummyText}
+
+ )
+ }, {
+ name: 'Avatar Card Title',
+ component: (
+
+
+
+
+
+
+
+
+ )
+ }
+];
class CardTest extends React.Component {
- onClick = () => {
- console.log('onClick', arguments);
- };
-
- onActionClick () {
- console.log('onClick', arguments);
- }
render () {
- const text = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.';
- const actions = [
- { label: 'Save', icon: 'add', className: 'flat accent', onClick: this.onActionClick.bind(this) },
- { label: 'Close', className: 'flat', onClick: this.onActionClick.bind(this) }];
-
return (
-
- Cards
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
Cards
+
+ {demos.map((demo, i) => (
+ -
+
+ {demo.component}
+
+
+ {demo.name}
+
+
+ ))}
+
+
);
}
}
diff --git a/spec/root.jsx b/spec/root.jsx
index e2b025e6..04dd63de 100644
--- a/spec/root.jsx
+++ b/spec/root.jsx
@@ -42,26 +42,9 @@ const Root = () => (
/>
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
);
diff --git a/spec/style.scss b/spec/style.scss
index 72a927b2..f3210e91 100644
--- a/spec/style.scss
+++ b/spec/style.scss
@@ -1,19 +1,16 @@
-$import-normalize: false;
-$import-flex-attributes: true;
-
-@import "../components/commons";
+@import "../components/base";
@import "../components/app_bar/config";
@import "../components/button/config";
$offset: 1.8 * $unit;
.app {
- padding: (8.2 * $unit) $offset $offset;
+ padding: (8.2 * $unit) $offset $offset $offset;
background-color: #f5f5f5;
> section {
padding: $offset;
margin-bottom: $offset;
- background-color: #fff;
+ background-color: white;
box-shadow: 0 1px 2px rgba(0,0,0, 0.25);
> h5 {
color: rgb(48, 63, 159);
@@ -51,3 +48,42 @@ $offset: 1.8 * $unit;
}
}
}
+
+.logo {
+ // height: $unit;
+}
+
+.demoList {
+ margin: 20px 0;
+ padding: 20px 0;
+ list-style: none;
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: stretch;
+}
+
+.demoListItem {
+ margin: 0 30px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.demo {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.demoName {
+ display: block;
+ margin: 20px 0 0;
+ text-align: center;
+}
+
+.card {
+ width: 345px;
+}