diff --git a/frontend/src/containers/services/service.js b/frontend/src/components/service-item/index.js similarity index 71% rename from frontend/src/containers/services/service.js rename to frontend/src/components/service-item/index.js index c6fd4e5a..2680f894 100644 --- a/frontend/src/containers/services/service.js +++ b/frontend/src/components/service-item/index.js @@ -1,12 +1,13 @@ const React = require('react'); -// const ReactRouter = require('react-router'); +const ReactRouter = require('react-router'); +const Anchor = require('@ui/components/anchor'); const List = require('@ui/components/list'); const PropTypes = require('@root/prop-types'); -// const { -// Link -// } = ReactRouter; +const { + Link +} = ReactRouter; const { ListItem, @@ -21,15 +22,16 @@ const { ListItemHeader } = List; -const Service = ({ +const ServiceItem = ({ org = '', project = '', service = {} }) => { - // const to = `/${org}/projects/${project}/services/${service.id}`; + const to = `/${org}/projects/${project}/services/${service.id}`; const childs = service.services.map((service) => ( 1} @@ -62,10 +64,21 @@ const Service = ({ ); return ( - + - {service.name} + + + {Anchor.fn( + + {service.name} + + )} + + {service.instances} instance @@ -75,10 +88,10 @@ const Service = ({ ); }; -Service.propTypes = { +ServiceItem.propTypes = { org: React.PropTypes.string, project: React.PropTypes.string, service: PropTypes.service }; -module.exports = Service; +module.exports = ServiceItem; diff --git a/frontend/src/containers/services/index.js b/frontend/src/containers/services/index.js index 94fcfe90..cc0c1c78 100644 --- a/frontend/src/containers/services/index.js +++ b/frontend/src/containers/services/index.js @@ -3,7 +3,7 @@ const ReactRedux = require('react-redux'); const EmptyServices = require('@components/empty/services'); const PropTypes = require('@root/prop-types'); -const Service = require('./service'); +const ServiceItem = require('@components/service-item'); const selectors = require('@state/selectors'); const { @@ -26,7 +26,7 @@ const Services = ({ ); const serviceList = services.map((service) => ( - state // somehow handleActions needs at least one reducer + [toggleServiceCollapsed.toString()]: (state, action) => ({ + ...state, + ui: { + ...state.ui, + collapsed: state.ui.collapsed.indexOf(action.payload) >= 0 + ? state.ui.collapsed.filter((uuid) => uuid !== action.payload) + : [...state.ui.collapsed, action.payload] + } + }) }, {}); diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index be168d92..f9da01c7 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -15,6 +15,7 @@ const serviceUiSections = (state) => get(state, 'services.ui.sections', []); const orgs = (state) => get(state, 'orgs.data', []); const projects = (state) => get(state, 'projects.data', []); const services = (state) => get(state, 'services.data', []); +const collapsedServices = (state) => get(state, 'services.ui.collapsed', []); const instances = (state) => get(state, 'instances.data', []); const projectById = (projectId) => createSelector( @@ -45,19 +46,28 @@ const orgSections = (orgId) => createSelector( ); const servicesByProjectId = (projectId) => createSelector( - [services, projectById(projectId)], - (services, project) => + [services, projectById(projectId), collapsedServices], + (services, project, collapsed) => services.filter((s) => s.project === project.uuid) .map((service) => ({ ...service, services: services.filter((s) => s.parent === service.uuid) })) .filter((s) => !s.parent) + .map((service) => ({ + ...service, + collapsed: collapsed.indexOf(service.uuid) >= 0, + services: service.services.map((service) => ({ + ...service, + collapsed: collapsed.indexOf(service.uuid) >= 0 + })) + })) ); const instancesByServiceId = (serviceId) => createSelector( [instances, serviceById(serviceId)], - (instances, service) => instances.filter((i) => i.service === service.uuid) + (instances, service) => + instances.filter((i) => i.service === service.uuid) ); diff --git a/ui/src/components/anchor/index.js b/ui/src/components/anchor/index.js new file mode 100644 index 00000000..758ea827 --- /dev/null +++ b/ui/src/components/anchor/index.js @@ -0,0 +1,26 @@ +const constants = require('../../shared/constants'); +const React = require('react'); +const Styled = require('styled-components'); + +const { + colors +} = constants; + +const { + default: styled +} = Styled; + +const color = (props) => props.secondary + ? colors.brandSecondaryLink + : colors.brandPrimaryLink; + +const Anchor = styled.a` + color: ${color} !important; +`; + +module.exports = Anchor; + +module.exports.fn = (element) => (props) => React.cloneElement(element, { + ...element.props, + ...props +}, element.props.children); diff --git a/ui/src/components/base/index.js b/ui/src/components/base/index.js index 10fbdf90..efcc93e9 100644 --- a/ui/src/components/base/index.js +++ b/ui/src/components/base/index.js @@ -5,9 +5,9 @@ const Styled = require('styled-components'); const { forms, - links, tables, - typography + typography, + colors } = constants; const { @@ -31,12 +31,12 @@ const fontFilenames = [ module.exports = styled.div` ${generateFonts(fontFamilies, fontFilenames)}; - + font-family: 'LibreFranklin', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 1rem; line-height: 1.5; - color: #373A3C; + color: ${colors.fonts.regular}; background-color: #FFFFFF; /************************************************************************** @@ -343,13 +343,12 @@ module.exports = styled.div` */ & a { - color: ${links.color}; - text-decoration: ${links.decoration}; + color: ${colors.brandPrimaryLink}; + text-decoration: underline; &:focus, &:hover { - color: ${links.hoverColor}; - text-decoration: ${links.hoverDecoration}; + text-decoration: none; } &:focus { @@ -364,8 +363,7 @@ module.exports = styled.div` &:focus, &:hover { - color: ${links.hoverColor}; - text-decoration: ${links.hoverDecoration}; + text-decoration: none; } &:focus { diff --git a/ui/src/components/close/index.js b/ui/src/components/close/index.js new file mode 100644 index 00000000..2f1c6aec --- /dev/null +++ b/ui/src/components/close/index.js @@ -0,0 +1,39 @@ +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const composers = require('../../shared/composers'); +const React = require('react'); +const Styled = require('styled-components'); +const closeIcon = require('../../shared/assets/close'); + +const { + colors +} = constants; + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +const Close = ({ + style, + onClick +}) => { + return ( + + Close + + ); +}; + +Close.propTypes = { + style: React.PropTypes.object, + onClick: React.PropTypes.func +}; + +module.exports = Close; diff --git a/ui/src/components/close/readme.md b/ui/src/components/close/readme.md new file mode 100644 index 00000000..a114f555 --- /dev/null +++ b/ui/src/components/close/readme.md @@ -0,0 +1,65 @@ +# `` + +## demo + +```embed +const React = require('react'); +const ReactDOM = require('react-dom/server'); +const Base = require('../base'); +const Container = require('../container'); +const Row = require('../row'); +const Column = require('../column'); +const Notificaton = require('./index.js'); +const styles = require('./style.css'); + +const style = { + marginBottom: 0 +} + +nmodule.exports = ReactDOM.renderToString( + + + + +

This is the warning content

+
+
+
+ + + +

This is the question content

+
+
+
+ + + +

This is the alert content

+
+
+
+ +); +``` + +## usage + +```js +const React = require('react'); +const Notificaton = require('ui/avatar'); + +module.exports = () => { + return ( + +

This is the warning content

+
+ +

This is the question content

+
+ +

This is the alert content

+
+ ); +} +``` diff --git a/ui/src/components/list/view.js b/ui/src/components/list/view.js index 7946b0e1..81796e35 100644 --- a/ui/src/components/list/view.js +++ b/ui/src/components/list/view.js @@ -16,26 +16,28 @@ const paddingTop = (props) => props.headed && !props.fromHeader ? remcalc(47) : remcalc(0); -const display = (props) => props.headed && !props.fromHeader && props.collapsed - ? 'none' - : 'flex'; - const StyledView = styled(Row)` flex: 1; margin: 0; height: 100%; padding-top: ${paddingTop}; - display: ${display}; `; -const View = (props) => ( - - {props.children} - -); +const View = (props) => { + const hide = props.headed && !props.fromHeader && props.collapsed; + + return hide ? null : ( + + {props.children} + + ); +}; View.propTypes = { - children: React.PropTypes.node + children: React.PropTypes.node, + collapsed: React.PropTypes.bool, + fromHeader: React.PropTypes.bool, + headed: React.PropTypes.bool }; module.exports = transferProps([ diff --git a/ui/src/components/mini-metric/graph.js b/ui/src/components/mini-metric/graph.js new file mode 100644 index 00000000..2cff5813 --- /dev/null +++ b/ui/src/components/mini-metric/graph.js @@ -0,0 +1,115 @@ +const buildArray = require('build-array'); +const Chart = require('chart.js'); +const fns = require('../../shared/functions'); +const React = require('react'); +const Styled = require('styled-components'); +const whisker = require('chartjs-chart-box-plot'); + +whisker(Chart); + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +const Container = styled.div` + position: relative; + height: ${remcalc(72)}; + width: 100%; +`; + +const Canvas = styled.canvas` + position: absolute; + bottom: 0; +`; + +class Graph extends React.Component { + componentDidMount() { + const { + data = [], + labels = 0, + max = 100, + min = 0 + } = this.props; + + const _labels = !Array.isArray(labels) + ? buildArray(labels || data.length).map((v, i) => '') + : labels; + + this._chart = new Chart(this._refs.component, { + type: 'whisker', + responsive: true, + maintainAspectRatio: true, + options: { + scales: { + xAxes: [{ + display: false, + barPercentage: 1.0, + categoryPercentage: 1.0 + }], + yAxes: [{ + display: false, + ticks: { + min: min, + max: max + } + }] + }, + legend: { + display: false + } + }, + data: { + labels: _labels, + datasets: [{ + data + }] + } + }); + } + componentWillReceiveProps(nextProps) { + const { + data = [], + labels = 0 + } = this.props; + + const _labels = !Array.isArray(labels) + ? buildArray(labels || data.length).map((v, i) => '') + : labels; + + this._chart.data.datasets = [{ + data + }]; + + this._chart.data.labels = _labels; + this._chart.update(0); + } + ref(name) { + this._refs = this._refs || {}; + + return (el) => { + this._refs[name] = el; + }; + } + render() { + return ( + + + + ); + } +} + +Graph.propTypes = { + data: React.PropTypes.array, + labels: React.PropTypes.number, + max: React.PropTypes.number, + min: React.PropTypes.number +}; + +module.exports = Graph; diff --git a/ui/src/components/mini-metric/index.js b/ui/src/components/mini-metric/index.js index 1ea1ce0d..96d27b77 100644 --- a/ui/src/components/mini-metric/index.js +++ b/ui/src/components/mini-metric/index.js @@ -1,166 +1,7 @@ -const React = require('react'); -const Styled = require('styled-components'); -const constants = require('../../shared/constants'); - -const { - colors -} = constants; - -const { - default: styled -} = Styled; - -const buildArray = require('build-array'); -const Chart = require('chart.js'); -const whisker = require('chartjs-chart-box-plot'); - -whisker(Chart); - -const StyledDiv = styled.div` - height: 127px; - width: 158px; - background-color: ${colors.miniBackground}; - border: solid 1px ${colors.borderSecondary}; - - &::before { - position: absolute; - z-index: 1; - width: 9px; - height: 127px; - background-image: - linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(216, 216, 216, 0)); - content: ''; - } -`; - -const Devider = styled.div` - width: 158px; - height: 1px; - background-color: ${colors.seperator} -`; - -const TextMetric = styled.div` - height: 38px; - padding: 8px 12px; -`; - -const InnerTextBox = styled.div` - width: 136px; - height: 36px; - font-family: 'Libre Franklin', sans-serif; - font-size: 12px; - font-weight: normal; - font-style: normal; - font-stretch: normal; - line-height: 18px; - text-align: right; - color: ${colors.regular}; - - & p { - margin: 0; - } - - & h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - line-height: 1.29; - color: ${colors.semibold}; - } -`; - -const StyledCanvas = styled.canvas` -`; - -class MiniMetric extends React.Component { - componentDidMount() { - const { - datasets = [], - labels = 0, - max = 100, - min = 0 - } = this.props; - - const _labels = !Array.isArray(labels) - ? buildArray(labels).map((v, i) => '') - : labels; - - this._chart = new Chart(this._refs.component, { - type: 'whisker', - responsive: true, - maintainAspectRatio: true, - options: { - scales: { - xAxes: [{ - display: false, - barPercentage: 1.0, - categoryPercentage: 1.0 - }], - yAxes: [{ - display: false, - ticks: { - min: min, - max: max - } - }] - }, - legend: { - display: false - } - }, - data: { - labels: _labels, - datasets: datasets - } - }); - } - componentWillReceiveProps(nextProps) { - const { - datasets = [], - labels = 0 - } = this.props; - - this._chart.data.datasets = datasets; - this._chart.data.labels = buildArray(labels).map((v, i) => ''); - this._chart.update(0); - } - ref(name) { - this._refs = this._refs || {}; - - return (el) => { - this._refs[name] = el; - }; - } - render() { - const { - name, - } = this.props; - - return ( - - - -

{name}: 54%

-

(1280/3000 MB)

-
-
- - -
- ); - } -} - -MiniMetric.propTypes = { - datasets: React.PropTypes.array, - labels: React.PropTypes.number, - max: React.PropTypes.number, - min: React.PropTypes.number, - name: React.PropTypes.string, +module.exports = { + MiniMetricGraph: require('./graph'), + MiniMetricMeta: require('./meta'), + MiniMetricTitle: require('./title'), + MiniMetricSubtitle: require('./subtitle'), + MiniMetricView: require('./view') }; - -module.exports = MiniMetric; diff --git a/ui/src/components/mini-metric/meta.js b/ui/src/components/mini-metric/meta.js new file mode 100644 index 00000000..985f2809 --- /dev/null +++ b/ui/src/components/mini-metric/meta.js @@ -0,0 +1,41 @@ +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const React = require('react'); +const Styled = require('styled-components'); + +const { + colors +} = constants; + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +const OuterBox = styled.div` + height: ${remcalc(53)}; + padding: ${remcalc(8)} ${remcalc(12)}; + border-bottom: ${remcalc(1)} solid ${colors.seperator}; +`; + +const InnerBox = styled.div` + width: 100%; + height: ${remcalc(36)}; +`; + +const Meta = (props) => ( + + + {props.children} + + +); + +Meta.propTypes = { + children: React.PropTypes.node +}; + +module.exports = Meta; diff --git a/ui/src/components/mini-metric/subtitle.js b/ui/src/components/mini-metric/subtitle.js new file mode 100644 index 00000000..119d076b --- /dev/null +++ b/ui/src/components/mini-metric/subtitle.js @@ -0,0 +1,26 @@ +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const Styled = require('styled-components'); + +const { + remcalc +} = fns; + +const { + colors +} = constants; + +const { + default: styled +} = Styled; + +module.exports = styled.p` + margin: 0; + text-align: right; + font-size: ${remcalc(12)}; + line-height: ${remcalc(18)}; + font-weight: normal; + font-style: normal; + font-stretch: normal; + color: ${colors.regular}; +`; diff --git a/ui/src/components/mini-metric/title.js b/ui/src/components/mini-metric/title.js new file mode 100644 index 00000000..339d7a44 --- /dev/null +++ b/ui/src/components/mini-metric/title.js @@ -0,0 +1,26 @@ +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const Styled = require('styled-components'); + +const { + colors +} = constants; + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +module.exports = styled.h3` + margin: 0; + text-align: right; + font-size: ${remcalc(14)}; + font-weight: 600; + font-style: normal; + line-height: 1.29; + color: ${colors.semibold}; + margin-bottom: ${remcalc(3)} !important; +`; diff --git a/ui/src/components/mini-metric/view.js b/ui/src/components/mini-metric/view.js new file mode 100644 index 00000000..f0d13005 --- /dev/null +++ b/ui/src/components/mini-metric/view.js @@ -0,0 +1,47 @@ +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const React = require('react'); +const Styled = require('styled-components'); + +const { + colors +} = constants; + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +const Container = styled.div` + position: relative; + height: 100%; + width: 100%; + background-color: ${colors.miniBackground}; + border: solid ${remcalc(1)} ${colors.borderSecondary}; +`; + +const Shadow = styled.div` + position: absolute; + height: 100%; + width: ${remcalc(9)}; + left: 0; + top: 0; + background-image: + linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(216, 216, 216, 0)); +`; + +const View = (props) => ( + + + {props.children} + +); + +View.propTypes = { + children: React.PropTypes.node +}; + +module.exports = View; \ No newline at end of file diff --git a/ui/src/components/notification/index.js b/ui/src/components/notification/index.js index dc1125d9..88e3258c 100644 --- a/ui/src/components/notification/index.js +++ b/ui/src/components/notification/index.js @@ -1,6 +1,6 @@ const constants = require('../../shared/constants'); const fns = require('../../shared/functions'); -const match = require('../../shared/match'); +const composers = require('../../shared/composers'); const React = require('react'); const Styled = require('styled-components'); @@ -12,44 +12,46 @@ const { remcalc } = fns; -const { - prop: matchProp -} = match; - const { default: styled } = Styled; -const background = matchProp({ - warning: colors.warningLight, - alert: colors.alertLight, -}, 'transparent'); +const { + baseBox, + pseudoEl +} = composers; -const border = matchProp({ - warning: colors.warning, - alert: 'red', -}, 'none'); +const decorationWidth = remcalc(108); const StyledNotification = styled.div` - border-radius: 4px; - box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.05); display: inline-block; - height: 100%; + min-height: 100%; + position: relative; + width: 100%; - background-color: ${background('type')}; - border: ${border('type')}; + ${baseBox(0)} + + &::before { + background-color: ${props => colors[props.type] || colors.brandPrimary} + width: ${decorationWidth}; + height: 100%; + + ${pseudoEl()} + } `; const StyledContent = styled.div` float: left; - padding: ${remcalc(20)}; + padding: ${remcalc(18)} 20% ${remcalc(18)} ${remcalc(18)}; + margin-left: ${decorationWidth}; + width: 100%; `; const Notificaton = ({ children, className, style, - type = '' + type }) => { return ( { - return Color(color).darken(0.15).hex(); - }, - hoverDecoration: 'underline' -}; - -module.exports = links; diff --git a/ui/src/shared/constants/typography.js b/ui/src/shared/constants/typography.js index f6ac5a6e..6e682734 100644 --- a/ui/src/shared/constants/typography.js +++ b/ui/src/shared/constants/typography.js @@ -1,8 +1,6 @@ const colors = require('./colors'); const typography = { - fontPrimary: 'sans serif', - dtFontWeight: 'bold', abbrBorderColor: colors.brandSecondary, textMuted: colors.brandSecondary }; diff --git a/ui/src/shared/functions.js b/ui/src/shared/functions.js index 57818b4a..c8c1512b 100644 --- a/ui/src/shared/functions.js +++ b/ui/src/shared/functions.js @@ -22,16 +22,16 @@ const generateFonts = (fontFamilies, fontFilenames) => { fontCSS += ` @font-face { font-family: ${fontFamily}; - src: url(${pathToFont}${fontFilenames[i]}.eot); - src: url(${pathToFont}${fontFilenames[i]}.eot?#iefix) + src: url(${pathToFont + fontFilenames[i]}.eot); + src: url(${pathToFont + fontFilenames[i]}.eot?#iefix) format('embedded-opentype'); - src: url(${pathToFont}${fontFilenames[i]}.woff) + src: url(${pathToFont + fontFilenames[i]}.woff) format('woff'); - src: url(${pathToFont}${fontFilenames[i]}.woff2) + src: url(${pathToFont + fontFilenames[i]}.woff2) format('woff2'); - src: url(${pathToFont}${fontFilenames[i]}.ttf) + src: url(${pathToFont + fontFilenames[i]}.ttf) format('truetype'); - src: url(${pathToFont}${fontFilenames[i]}.svg#${fontFamily}) + src: url(${pathToFont + fontFilenames[i]}.svg#${fontFamily}) format('svg'); font-weight: normal; font-style: normal; diff --git a/ui/stories/index.js b/ui/stories/index.js index a2ec3d10..e255249b 100644 --- a/ui/stories/index.js +++ b/ui/stories/index.js @@ -33,7 +33,13 @@ const { ListItemView, ListItemGroupView }, - MiniMetric, + MiniMetric: { + MiniMetricGraph, + MiniMetricMeta, + MiniMetricTitle, + MiniMetricSubtitle, + MiniMetricView + }, Modal, Notificaton, Pagination, @@ -72,7 +78,7 @@ const styles = { storiesOf('Grid', module) .add('Row and Column', () => ( - + ( - + + + )) .add('type=email', () => ( - - We'll never share your email with anyone else. - + + + We'll never share your email with anyone else. + + )); storiesOf('Modal', module) @@ -241,19 +251,25 @@ storiesOf('Modal', module) storiesOf('Notificaton', module) .add('Default', () => ( - - This is the default content - + + + This is the default content + + )) - .add('warning', () => ( - - This is the warning content - + .add('Success', () => ( + + + This is the success content + + )) - .add('alert', () => ( - - This is the alert content - + .add('Alert', () => ( + + + This is the alert content + + )); storiesOf('Pagination', module) @@ -399,141 +415,143 @@ storiesOf('Widget', module) )); -const colors = { - perc: 'rgba(54, 74, 205, 0.2)', - alt: 'rgba(245, 93, 93, 0.2)' -}; +const minMetricData = [{ + firstQuartile: 15, + thirdQuartile: 15, + median: 15, + max: 15, + min: 15, +}, { + firstQuartile: 26, + thirdQuartile: 26, + median: 26, + max: 26, + min: 26, +}, { + firstQuartile: 17, + thirdQuartile: 17, + median: 17, + max: 17, + min: 17, +}, { + firstQuartile: 15, + thirdQuartile: 25, + median: 19, + max: 19, + min: 20, +}, { + firstQuartile: 19, + thirdQuartile: 25, + median: 21, + max: 20, + min: 25, +}, { + firstQuartile: 24, + thirdQuartile: 30, + median: 25, + max: 26, + min: 27, +}, { + firstQuartile: 28, + thirdQuartile: 34, + median: 30, + max: 30, + min: 30, +}, { + firstQuartile: 30, + thirdQuartile: 45, + median: 35, + max: 40, + min: 40, +}, { + firstQuartile: 20, + thirdQuartile: 55, + median: 45, + max: 44, + min: 44, +}, { + firstQuartile: 55, + thirdQuartile: 55, + median: 55, + max: 55, + min: 55, +}, { + firstQuartile: 57, + thirdQuartile: 56, + median: 57, + max: 58, + min: 57, +}, { + firstQuartile: 57, + thirdQuartile: 56, + median: 56, + max: 56, + min: 56, +}, { + firstQuartile: 60, + thirdQuartile: 56, + median: 60, + max: 60, + min: 60, +}, { + firstQuartile: 57, + thirdQuartile: 57, + median: 57, + max: 57, + min: 57, +}, { + firstQuartile: 57, + thirdQuartile: 55, + median: 55, + max: 55, + min: 55, +}, { + firstQuartile: 20, + thirdQuartile: 45, + median: 45, + max: 45, + min: 45, +}, { + firstQuartile: 15, + thirdQuartile: 40, + median: 30, + max: 49, + min: 30, +}]; + storiesOf('Metrics', module) .add('Mini Metric', () => ( - + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + )); storiesOf('ListItem', module)