Added <Avatar> component as required by the card title component

old
Nathan Marks 2015-11-17 16:24:22 -05:00
parent 37199c22e6
commit dba68e72a5
17 changed files with 536 additions and 219 deletions

View File

@ -227,7 +227,7 @@
"react/no-danger": 0, "react/no-danger": 0,
"react/no-did-mount-set-state": 0, "react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 1, "react/no-did-update-set-state": 1,
"react/no-multi-comp": 0, "react/no-multi-comp": 1,
"react/no-unknown-property": 1, "react/no-unknown-property": 1,
"react/prop-types": [2, {"ignore": ["onMouseDown", "onTouchStart"]}], "react/prop-types": [2, {"ignore": ["onMouseDown", "onTouchStart"]}],
"react/react-in-jsx-scope": 1, "react/react-in-jsx-scope": 1,

View File

@ -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 = <img className={style.avatarImg} src={image} />;
} else if (typeof image === 'string') {
component = <img className={style.avatarImg} src={image} />;
} else if (typeof icon === 'string') {
component = <FontIcon value="icon" />;
}
return (
<div className={classes}>{component ? component : children}</div>
);
}
}
export default Avatar;

View File

@ -0,0 +1 @@
export Avatar from './Avatar.jsx';

View File

@ -0,0 +1,11 @@
.avatar {
width: 40px;
height: 40px;
overflow: hidden;
border-radius: 50%;
}
.avatarImg {
max-width: 100%;
height: auto;
}

View File

@ -1,87 +1,36 @@
import React from 'react'; import React, { PropTypes, Component } from 'react';
import Navigation from '../navigation'; import ClassNames from '../decorators/ClassNames';
import Ripple from '../ripple'; import styles from './style';
import style from './style';
@ClassNames(styles)
class Card extends Component {
class Card extends React.Component {
static propTypes = { static propTypes = {
actions: React.PropTypes.array, children: PropTypes.any,
className: React.PropTypes.string, className: PropTypes.string,
color: React.PropTypes.string, classNames: PropTypes.func,
image: React.PropTypes.string, raised: PropTypes.bool
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 (
<Navigation data-role='actions' className={style.navigation} actions={this.props.actions} />
);
}
}
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 (
<figure className={style.figure} style={styleFigure}>
{this.props.title ? <h5 data-role='title'>{this.props.title}</h5> : null}
{this.props.subtitle ? <small data-role='subtitle'>{this.props.subtitle}</small> : null}
{this.props.color ? <div className={style.overflow} style={styleOverflow}></div> : null}
</figure>
);
}
} }
render () { render () {
let className = style.root; const {
if (this.props.type) className += ` ${style[this.props.type]}`; children,
if (this.props.onClick) className += ` ${style.touch}`; className,
if (this.props.image || this.props.color) className += ` ${style.contrast}`; classNames,
if (this.props.color) className += ` ${style.color}`; raised,
if (this.props.loading) className += ` ${style.loading}`; ...otherProps
if (this.props.className) className += ` ${this.props.className}`; } = this.props;
const classes = classNames('card', {
'raised': raised
}, className);
return ( return (
<div <div
data-react-toolbox='card' data-react-toolbox="card"
className={className} className={classes}
onMouseDown={this.handleMouseDown}
> >
{this.renderTitle()} {children}
{this.props.text ? <p data-role='text' className={style.text}>{this.props.text}</p> : null}
{this.renderActions()}
<Ripple
ref='ripple'
className={style.ripple}
loading={this.props.loading}
spread={2.5}
/>
</div> </div>
); );
} }

View File

@ -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 (
<div className={classes} {...otherProps}>
{children}
</div>
);
}
}
export default CardActions;

View File

@ -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 (
<div style={bgStyle} className={classes} {...otherProps}>
<div className={style.content}>
{children}
</div>
</div>
);
}
}
export default CardMedia;

View File

@ -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 (
<div className={classes} {...otherProps}>
{typeof children === 'string' ? <p>{children}</p> : children}
</div>
);
}
}
export default CardText;

View File

@ -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 = <Avatar image={avatar} />;
} else {
avatarComponent = avatar;
}
return (
<div className={classes} {...otherProps}>
{avatarComponent && (
<div className={style.avatar}>
{avatarComponent}
</div>
)}
<div>
{(title || children) && <h5 className={style.title}>{title ? title : children}</h5>}
{subtitle && <p className={style.subtitle}>{subtitle}</p>}
</div>
</div>
);
}
}
export default CardTitle;

View File

@ -1,7 +1,10 @@
$card-color-white: $color-white !default; $card-color-white: unquote("rgb(#{$color-white})") !default;
$card-title-height: 17.6 * $unit; $card-text-overlay: unquote("rgba(#{$color-black}, 0.2)");
$card-width-normal: 32 * $unit;
$card-width-large: 51.2 * $unit; $card-background-color: $card-color-white;
$card-offset: 1.6 * $unit;
$card-navigation-offset: $card-offset / 2; $card-padding-sm: 8px;
$card-text-overlay: rgba($color-black, 0.2); $card-padding: 16px;
$card-padding-lg: 20px;
$card-font-size: $font-size-small;

View File

@ -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';

View File

@ -1,112 +1,122 @@
@import "../base"; @import "../base";
@import "./config"; @import "./config";
.figure { .card {
position: relative; // Box Model
display: flex; display: flex;
min-height: $card-title-height;
flex-direction: column; flex-direction: column;
justify-content: flex-end; border-radius: 2px;
background-position: center center; overflow: hidden;
background-size: cover;
> *:not(.overflow) { // Fonts
z-index: $z-index-normal; font-size: $card-font-size;
font-weight: $font-weight-normal;
// 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; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 0;
// Box Model
display: flex;
flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: .75;
} }
} }
.text { .cardTitle {
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;
display: flex; display: flex;
width: $card-width-normal; align-items: center;
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;
}
}
.touch { .avatar {
cursor: pointer; margin-right: 13px;
} }
.contrast { .subtitle {
.figure { color: $color-text-secondary;
color: $card-color-white;
text-shadow: 0;
} }
.ripple {
background-color: $card-color-white;
}
}
.loading { &.large {
pointer-events: none; padding: $card-padding-lg $card-padding ($card-padding - 2px);
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;
}
}
.image { .title {
&, .figure { @include typo-headline();
height: $card-width-normal; line-height: 1.25;
}
} }
.figure {
padding: 0; &.small {
> h5 { padding: $card-padding;
padding: $card-offset;
font-size: $font-size-small; .title {
font-weight: $font-weight-bold; @include typo-body-2(false, true);
background-color: $card-text-overlay; line-height: 1.4;
}
.subtitle {
font-weight: 500;
line-height: 1.4;
} }
} }
} }
.event { .cardTitle, .cardText {
.figure { padding: ($card-padding - 2px) $card-padding;
justify-content: flex-start;
&:last-child {
padding-bottom: $card-padding-lg;
}
& + .cardText {
padding-top: 0;
} }
} }
.wide { .cardActions {
width: $card-width-large; 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;
}
}
} }

View File

@ -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 (
<ComposedComponent
classNames={classNamesFn}
{...props}
/>
);
};
};
}
export default classNames;

View File

@ -1,37 +1,85 @@
import React from 'react'; 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: (
<Card className={style.card}>
<CardTitle>Basic Card</CardTitle>
<CardText>{dummyText}</CardText>
<CardActions>
<Button label="Action" />
<Button label="More" />
</CardActions>
</Card>
)
}, {
name: '16:9 Card Media',
component: (
<Card className={style.card}>
<CardMedia
aspectRatio="wide"
image="https://placeimg.com/800/450/nature"
/>
<CardTitle
title="Basic Card"
subtitle="An awesome basic card"
/>
<CardText>{dummyText}</CardText>
</Card>
)
}, {
name: 'Avatar Card Title',
component: (
<Card className={style.card}>
<CardTitle
avatar="https://placeimg.com/80/80/animals"
title="Avatar Card"
subtitle="An awesome basic card"
/>
<CardMedia
aspectRatio="wide"
image="https://placeimg.com/800/450/nature"
/>
<CardActions style={{ justifyContent: 'flex-end' }}>
<Button icon="feedback" />
<Button icon="favorite" />
</CardActions>
</Card>
)
}
];
class CardTest extends React.Component { class CardTest extends React.Component {
onClick = () => {
console.log('onClick', arguments);
};
onActionClick () {
console.log('onClick', arguments);
}
render () { 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 ( return (
<section> <div>
<h5>Cards</h5> <h2>Cards</h2>
<Card title='Default Card' /> <ul className={style.demoList}>
<Card title='Default Card loading' loading /> {demos.map((demo, i) => (
<Card type='wide' title='Wide card' /> <li key={i} className={style.demoListItem}>
<Card title='Default Card with text' text={text} /> <div className={style.demo}>
<Card title='Default Card with actions' actions={actions} /> {demo.component}
<Card title='Default Card with text and image' text={text} image='https://avatars2.githubusercontent.com/u/559654?v=3&s=460' /> </div>
<Card title='Default Card with text, image and color' text={text} color='#e91e63' image='https://avatars2.githubusercontent.com/u/559654?v=3&s=460' /> <div className={style.demoName}>
<Card title='Default Card with text, image and color' text={text} color='#00bcd4' image='https://avatars2.githubusercontent.com/u/1634922?v=3&s=460' /> {demo.name}
<Card title='Default Card with text, color and onClick event' text={text} color='#e91e63' onClick={this.onClick} /> </div>
<Card type='wide' title='Wide Card loading with text, color and onClick event' text={text} color='#e91e63' onClick={this.onClick} loading /> </li>
<Card type='image' title='javivelasco.jpg' image='https://avatars2.githubusercontent.com/u/1634922?v=3&s=460' /> ))}
<Card type='event' title='Featured event: May 24, 2016 7-11pm' color='#00bcd4' /> </ul>
</section> </div>
); );
} }
} }

View File

@ -42,26 +42,9 @@ const Root = () => (
/> />
</AppBarToolbox> </AppBarToolbox>
<Autocomplete />
<Button />
<Card /> <Card />
<Checkbox />
<Dialog />
<Drawer />
<Dropdown />
<IconMenu />
<Input />
<List />
<Menu />
<Pickers />
<Progress />
<Radio />
<Slider />
<Snackbar />
<Switch />
<Table />
<Tabs />
<Tooltip />
</App> </App>
); );

View File

@ -1,19 +1,16 @@
$import-normalize: false; @import "../components/base";
$import-flex-attributes: true;
@import "../components/commons";
@import "../components/app_bar/config"; @import "../components/app_bar/config";
@import "../components/button/config"; @import "../components/button/config";
$offset: 1.8 * $unit; $offset: 1.8 * $unit;
.app { .app {
padding: (8.2 * $unit) $offset $offset; padding: (8.2 * $unit) $offset $offset $offset;
background-color: #f5f5f5; background-color: #f5f5f5;
> section { > section {
padding: $offset; padding: $offset;
margin-bottom: $offset; margin-bottom: $offset;
background-color: #fff; background-color: white;
box-shadow: 0 1px 2px rgba(0,0,0, 0.25); box-shadow: 0 1px 2px rgba(0,0,0, 0.25);
> h5 { > h5 {
color: rgb(48, 63, 159); 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;
}