SCSS Pipeline and style fixes (#229)

* Add SCSS pipeline and fix login style issues

* Fix nav styles and make tests pass

* Fix nav header styles and animations

* Change font-size to 13px on nav

* Fix duplicate specificity of styles
This commit is contained in:
Jason Meller 2016-09-23 14:04:01 -04:00 committed by GitHub
parent 1d5596941a
commit 55307de42d
23 changed files with 220 additions and 134 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ node_modules
# generated artifacts
assets/bundle.js
assets/bundle.css
server/bindata.go
*.cover
*.test

View file

@ -3,7 +3,6 @@ export default {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
paddingTop: '80px',
marginTop: '13vh',
},
};

View file

@ -4,6 +4,7 @@ import { isEqual, last } from 'lodash';
import componentStyles from './styles';
import kolideLogo from '../../../assets/images/kolide-logo.svg';
import navItems from './navItems';
import './styles.scss';
class SidePanel extends Component {
static propTypes = {
@ -41,6 +42,10 @@ class SidePanel extends Component {
};
}
setSubNavClass = (showSubItems) => {
return showSubItems ? 'sub-nav sub-nav--expanded' : 'sub-nav';
}
toggleShowSubItems = (showSubItems) => {
return (evt) => {
evt.preventDefault();
@ -64,6 +69,7 @@ class SidePanel extends Component {
orgNameStyles,
usernameStyles,
userStatusStyles,
orgChevronStyles,
} = componentStyles;
return (
@ -76,6 +82,7 @@ class SidePanel extends Component {
<h1 style={orgNameStyles}>Kolide, Inc.</h1>
<div style={userStatusStyles(enabled)} />
<h2 style={usernameStyles}>{username}</h2>
<i style={orgChevronStyles} className="kolidecon-chevron-bold-down" />
</header>
);
}
@ -97,6 +104,7 @@ class SidePanel extends Component {
<div style={navItemWrapperStyles(lastChild)} key={`nav-item-${name}`}>
{active && <div style={navItemBeforeStyles} />}
<li
key={name}
onClick={setActiveTab(name)}
style={navItemStyles(active)}
>
@ -146,6 +154,7 @@ class SidePanel extends Component {
>
{active && <div style={subItemBeforeStyles} />}
<li
key={name}
onClick={setActiveSubItem(name)}
style={subItemStyles(active)}
>
@ -157,13 +166,11 @@ class SidePanel extends Component {
renderSubItems = (subItems) => {
const { subItemListStyles, subItemsStyles } = componentStyles;
const { renderCollapseSubItems, renderSubItem } = this;
const { renderCollapseSubItems, renderSubItem, setSubNavClass } = this;
const { showSubItems } = this.state;
if (!subItems.length) return false;
return (
<div style={subItemsStyles(showSubItems)}>
<div className={setSubNavClass(showSubItems)} style={subItemsStyles}>
<ul style={subItemListStyles(showSubItems)}>
{subItems.map(subItem => {
return renderSubItem(subItem);
@ -181,8 +188,8 @@ class SidePanel extends Component {
const iconName = showSubItems ? 'kolidecon-chevron-bold-left' : 'kolidecon-chevron-bold-right';
return (
<div style={collapseSubItemsWrapper}>
<i className={iconName} style={{ color: '#FFF' }} onClick={toggleShowSubItems(!showSubItems)} />
<div style={collapseSubItemsWrapper} onClick={toggleShowSubItems(!showSubItems)}>
<i className={iconName} />
</div>
);
}

View file

@ -5,21 +5,43 @@ const { border, color, font, padding } = Styles;
const componentStyles = {
companyLogoStyles: {
position: 'absolute',
left: '16px',
height: '44px',
left: '0',
top: '23px',
height: '42px',
marginRight: '10px',
borderColor: color.accentMedium,
borderStyle: 'solid',
borderWidth: '1px',
borderRadius: '100%',
'@media (max-width: 760px)': {
left: '4px',
left: '5px',
},
},
headerStyles: {
borderBottomColor: color.accentLight,
borderBottomStyle: 'solid',
borderBottomWidth: '1px',
height: '67px',
marginBottom: padding.half,
marginRight: padding.medium,
height: '62px',
cursor: 'pointer',
paddingLeft: '54px',
paddingTop: '26px',
marginRight: '16px',
position: 'relative',
},
orgChevronStyles: {
color: color.accentMedium,
fontSize: '12px',
position: 'absolute',
top: '50px',
right: '35px',
'@media (max-width: 760px)': {
top: 'auto',
left: '0',
right: '0',
bottom: '6px',
textAlign: 'center',
display: 'block',
},
},
iconStyles: {
position: 'relative',
@ -28,7 +50,9 @@ const componentStyles = {
top: '4px',
left: 0,
'@media (max-width: 760px)': {
left: '5px',
display: 'block',
textAlign: 'center',
marginRight: 0,
},
},
navItemBeforeStyles: {
@ -36,8 +60,8 @@ const componentStyles = {
width: '6px',
height: '50px',
position: 'absolute',
left: '-24px',
top: '2px',
left: '-16px',
top: 0,
bottom: 0,
backgroundColor: '#9a61c6',
'@media (max-width: 760px)': {
@ -60,10 +84,11 @@ const componentStyles = {
const activeStyles = {
color: color.brand,
borderBottom: 'none',
transition: 'none',
':hover': {
color: color.brandDark,
},
'@media (max-width: 760px)': {
borderBottom: '8px solid #9a61c6',
textAlign: 'center',
borderBottom: '6px solid #9a61c6',
},
};
@ -72,12 +97,13 @@ const componentStyles = {
position: 'relative',
color: color.textLight,
cursor: 'pointer',
fontSize: font.small,
fontSize: '13px',
letterSpacing: '0.5px',
textTransform: 'uppercase',
paddingTop: padding.half,
transition: 'all 0.2s ease-in-out',
'@media (max-width: 760px)': {
textAlign: 'center',
transition: 'color 0.2s ease-in-out',
':hover': {
color: color.textDark,
},
};
@ -122,25 +148,24 @@ const componentStyles = {
bottom: 0,
boxShadow: '2px 0 8px 0 rgba(0, 0, 0, 0.1)',
left: 0,
paddingLeft: padding.large,
paddingTop: padding.large,
paddingLeft: '16px',
position: 'fixed',
top: 0,
width: '216px',
width: '223px',
'@media (max-width: 760px)': {
paddingLeft: 0,
width: '54px',
},
},
orgNameStyles: {
fontSize: font.medium,
fontSize: '16px',
letterSpacing: '0.5px',
margin: 0,
overFlow: 'hidden',
padding: 0,
position: 'relative',
textOverflow: 'ellipsis',
top: '3px',
top: '1px',
whiteSpace: 'nowrap',
'@media (max-width: 760px)': {
display: 'none',
@ -167,8 +192,12 @@ const componentStyles = {
},
subItemStyles: (active) => {
const activeStyles = {
fontSize: '13px',
fontWeight: font.weight.bold,
opacity: '1',
':hover': {
opacity: '1.0',
},
};
const baseStyles = {
@ -184,6 +213,9 @@ const componentStyles = {
position: 'relative',
textTransform: 'none',
transition: 'all 0.2s ease-in-out',
':hover': {
opacity: '0.75',
},
};
if (active) {
@ -195,54 +227,62 @@ const componentStyles = {
return baseStyles;
},
subItemsStyles: (expanded) => {
return {
backgroundColor: color.brand,
boxShadow: 'inset 0 5px 8px 0 rgba(0, 0, 0, 0.12), inset 0 -5px 8px 0 rgba(0, 0, 0, 0.12)',
marginBottom: 0,
marginRight: 0,
minHeight: '87px',
paddingBottom: padding.half,
paddingTop: padding.half,
marginLeft: '-24px',
marginTop: padding.medium,
transition: 'width 0.1s ease-in-out',
'@media (max-width: 760px)': {
bottom: '-8px',
left: '54px',
marginLeft: 0,
position: 'absolute',
width: expanded ? '251px' : '18px',
},
};
subItemsStyles: {
backgroundColor: color.brand,
boxShadow: 'inset 0 5px 8px 0 rgba(0, 0, 0, 0.12), inset 0 -5px 8px 0 rgba(0, 0, 0, 0.12)',
marginRight: 0,
marginBottom: '6px',
paddingBottom: '3px',
paddingTop: '3px',
marginLeft: '-16px',
position: 'relative',
top: '10px',
transition: 'width 0.1s ease-in-out',
'@media (max-width: 760px)': {
minHeight: '84px',
borderTopRightRadius: '3px',
borderBottomRightRadius: '3px',
boxShadow: '2px 2px 8px rgba(0,0,0,0.1)',
bottom: '-8px',
left: '54px',
marginLeft: 0,
position: 'absolute',
},
},
subItemListStyles: (expanded) => {
return {
listStyle: 'none',
paddingLeft: '16px',
'@media (max-width: 760px)': {
borderRight: '1px solid rgba(0,0,0,0.16)',
display: expanded ? 'inline-block' : 'none',
padding: 0,
textAlign: 'left',
width: '211px',
width: '166px',
},
};
},
collapseSubItemsWrapper: {
position: 'absolute',
right: '3px',
top: '41%',
right: '4px',
top: '0',
bottom: '0',
lineHeight: '95px',
color: '#fff',
'@media (min-width: 761px)': {
display: 'none',
},
},
usernameStyles: {
position: 'relative',
top: '3px',
display: 'inline-block',
margin: 0,
padding: 0,
fontSize: font.small,
top: '-3px',
left: '4px',
fontSize: '13px',
letterSpacing: '0.6px',
textTransform: 'uppercase',
'@media (max-width: 760px)': {
display: 'none',
@ -257,10 +297,8 @@ const componentStyles = {
borderRadius: border.radius.circle,
display: 'inline-block',
height: size,
left: '1px',
marginRight: '6px',
position: 'relative',
top: '6px',
width: size,
'@media (max-width: 760px)': {
display: 'none',

View file

@ -0,0 +1,8 @@
@media (max-width: 760px) {
.sub-nav {
width: 22px;
}
.sub-nav--expanded {
width: 188px;
}
}

View file

@ -24,7 +24,7 @@ class StackedWhiteBoxes extends Component {
return (
<div style={exWrapperStyles}>
<Link style={exStyles} to={previousLocation}>x</Link>
<Link style={exStyles} to={previousLocation}></Link>
</div>
);
}

View file

@ -4,16 +4,15 @@ const { border, color, font, padding } = styles;
export default {
boxStyles: {
alignItems: 'center',
backgroundColor: color.white,
borderTopLeftRadius: border.radius.base,
borderTopRightRadius: border.radius.base,
borderRadius: border.radius.base,
boxShadow: border.shadow.blur,
minHeight: '370px',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
padding: padding.base,
width: '522px',
padding: '30px',
width: '524px',
position: 'relative',
fontWeight: 300,
},
containerStyles: {
alignItems: 'center',
@ -24,6 +23,10 @@ export default {
exStyles: {
color: color.lightGrey,
textDecoration: 'none',
position: 'absolute',
top: '30px',
right: '30px',
fontWeight: 'bold',
},
exWrapperStyles: {
textAlign: 'right',
@ -53,6 +56,8 @@ export default {
textStyles: {
color: color.purpleGrey,
fontSize: font.medium,
lineHeight: '30px',
letterSpacing: '0.64px',
},
smallTabStyles: {
backgroundColor: color.white,

View file

@ -16,7 +16,6 @@ export default {
fontSize: font.large,
fontWeight: '300',
letterSpacing: '4px',
marginTop: padding.base,
padding: padding.medium,
position: 'relative',
textTransform: 'uppercase',

View file

@ -86,7 +86,7 @@ class ForgotPasswordForm extends Component {
render () {
const { error: serverError } = this.props;
const { errors: clientErrors } = this.state;
const { formStyles, inputStyles, submitButtonStyles } = componentStyles;
const { formStyles, submitButtonContainerStyles, submitButtonStyles } = componentStyles;
const { onFormSubmit, onInputFieldChange } = this;
return (
@ -94,17 +94,18 @@ class ForgotPasswordForm extends Component {
<InputFieldWithIcon
autofocus
error={clientErrors.email || serverError}
iconName="envelope"
iconName="kolidecon-email"
name="email"
onChange={onInputFieldChange}
placeholder="Email Address"
style={inputStyles}
/>
<GradientButton
type="submit"
style={submitButtonStyles}
text="Reset Password"
/>
<div style={submitButtonContainerStyles}>
<GradientButton
type="submit"
text="Reset Password"
style={submitButtonStyles}
/>
</div>
</form>
);
}

View file

@ -2,7 +2,15 @@ export default {
formStyles: {
width: '100%',
},
inputStyles: {
width: '100%',
submitButtonStyles: {
':active': {
boxShadow: '0 1px 0 #734893',
},
},
submitButtonContainerStyles: {
position: 'absolute',
bottom: '30px',
left: '30px',
right: '30px',
},
};

View file

@ -140,14 +140,14 @@ class LoginForm extends Component {
<InputFieldWithIcon
autofocus
error={errors.username}
iconName="user"
iconName="kolidecon-username"
name="username"
onChange={onInputChange('username')}
placeholder="Username or Email"
/>
<InputFieldWithIcon
error={errors.password}
iconName="lock"
iconName="kolidecon-password"
name="password"
onChange={onInputChange('password')}
placeholder="Password"

View file

@ -12,19 +12,21 @@ export default {
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
padding: padding.base,
padding: '30px',
width: FORM_WIDTH,
minHeight: '350px',
fontWeight: '300',
},
forgotPasswordStyles: {
fontSize: font.medium,
letterSpacing: '1px',
textDecoration: 'none',
color: color.accentText,
},
forgotPasswordWrapperStyles: {
marginTop: padding.base,
textAlign: 'right',
width: '378px',
width: '100%',
},
formStyles: {
boxShadow: '0 5px 30px 0 rgba(0,0,0,0.30)',
@ -32,7 +34,6 @@ export default {
submitButtonStyles: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
marginTop: 0,
padding: padding.base,
width: FORM_WIDTH,
},

View file

@ -1,8 +1,8 @@
import React, { Component, PropTypes } from 'react';
import radium from 'radium';
import Icon from '../../../icons/Icon';
import componentStyles from './styles';
class InputFieldWithIcon extends Component {
static propTypes = {
autofocus: PropTypes.bool,
@ -45,17 +45,6 @@ class InputFieldWithIcon extends Component {
return onChange(evt);
}
iconVariant = () => {
const { error } = this.props;
const { value } = this.state;
if (error) return 'error';
if (value) return 'colored';
return 'default';
}
renderHeading = () => {
const { error, placeholder } = this.props;
const { value } = this.state;
@ -70,9 +59,9 @@ class InputFieldWithIcon extends Component {
render () {
const { error, iconName, name, placeholder, style, type } = this.props;
const { containerStyles, iconStyles, inputErrorStyles, inputStyles } = componentStyles;
const { containerStyles, iconStyles, iconErrorStyles, inputErrorStyles, inputStyles } = componentStyles;
const { value } = this.state;
const { iconVariant, onInputChange } = this;
const { onInputChange } = this;
return (
<div style={containerStyles}>
@ -80,12 +69,13 @@ class InputFieldWithIcon extends Component {
<input
name={name}
onChange={onInputChange}
className="input-with-icon"
placeholder={placeholder}
ref={(r) => { this.input = r; }}
style={[inputStyles(value), inputErrorStyles(error), style]}
style={[inputStyles(value, type), inputErrorStyles(error), style]}
type={type}
/>
<Icon name={iconName} style={iconStyles} variant={iconVariant()} />
<i className={iconName} style={[iconStyles(value), iconErrorStyles(error), style]} />
</div>
);
}

View file

@ -6,16 +6,38 @@ export default {
containerStyles: {
marginTop: padding.base,
position: 'relative',
width: '100%',
},
errorStyles: {
color: color.alert,
fontSize: font.small,
textTransform: 'lowercase',
},
iconStyles: {
position: 'absolute',
right: '6px',
top: '29px',
iconStyles: (value) => {
const baseStyles = {
position: 'absolute',
right: '6px',
top: '28px',
fontSize: '20px',
color: color.accentText,
};
if (value) {
return {
...baseStyles,
color: color.brand,
};
}
return baseStyles;
},
iconErrorStyles: (error) => {
if (error) {
return {
color: color.alert,
};
}
return false;
},
inputErrorStyles: (error) => {
if (error) {
@ -26,21 +48,35 @@ export default {
return {};
},
inputStyles: (value) => {
inputStyles: (value, type) => {
const baseStyles = {
borderLeft: 'none',
borderRight: 'none',
borderTop: 'none',
borderBottomWidth: '1px',
borderBottomWidth: '2px',
fontSize: '20px',
borderBottomStyle: 'solid',
borderBottomColor: color.brand,
borderBottomColor: color.brandUltralight,
color: color.accentText,
width: '378px',
paddingRight: '30px',
opacity: '1',
textIndent: '1px',
position: 'relative',
width: '100%',
boxSizing: 'border-box',
':focus': {
outline: 'none',
},
};
if (type === 'password' && value) {
return {
...baseStyles,
letterSpacing: '7px',
color: color.textUltradark,
};
}
if (value) {
return {
...baseStyles,

View file

@ -1,5 +1,6 @@
import ReactDOM from 'react-dom';
import routes from './router';
import './index.scss';
if (typeof window !== 'undefined') {
const { document } = global;

3
frontend/index.scss Normal file
View file

@ -0,0 +1,3 @@
@import "stylesheets/fonts.scss";
@import "stylesheets/icons.scss";
@import "stylesheets/forms.scss";

View file

@ -6,13 +6,11 @@ import componentStyles from './styles';
import Icon from '../../components/icons/Icon';
import paths from '../../router/paths';
const COUNTDOWN_INTERVAL = 1000;
const REDIRECT_TIME = 3000;
const REDIRECT_TIME = 1200;
class LoginSuccessfulPage extends Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
user: PropTypes.object,
};
constructor (props) {
@ -27,42 +25,23 @@ class LoginSuccessfulPage extends Component {
this.startRedirectCountdown();
}
componentWillUnmount () {
const { interval } = this;
if (interval) clearInterval(interval);
}
startRedirectCountdown = () => {
const { dispatch } = this.props;
const { HOME } = paths;
const { redirectTime } = this.state;
this.interval = setInterval(() => {
const { redirectTime } = this.state;
if (redirectTime > 0) {
this.setState({
redirectTime: redirectTime - COUNTDOWN_INTERVAL,
});
return false;
}
setTimeout(() => {
return dispatch(push(HOME));
}, COUNTDOWN_INTERVAL);
}, redirectTime);
}
render () {
const { loginSuccessStyles, subtextStyles, whiteBoxStyles } = componentStyles;
const { redirectTime } = this.state;
const secondsToRedirect = redirectTime / 1000;
return (
<div style={whiteBoxStyles}>
<Icon name="check" />
<p style={loginSuccessStyles}>Login successful</p>
<p style={subtextStyles}>Hold on to your butts.</p>
<p style={subtextStyles}>redirecting in {secondsToRedirect}</p>
<p style={subtextStyles}>hold on to your butts...</p>
</div>
);
}

View file

@ -0,0 +1,4 @@
.input-with-icon::placeholder {
color: #A8B1CD;
opacity: 1;
}

View file

@ -2,8 +2,7 @@
<html data-uuid="{{ .UUID }}">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="/assets/stylesheets/fonts.css">
<link rel="stylesheet" type="text/css" href="/assets/stylesheets/icons.css">
<link rel="stylesheet" type="text/css" href="/assets/bundle.css">
<title>Kolide</title>
</head>
<body>

View file

@ -34,6 +34,7 @@
"jsdom": "^9.5.0",
"lodash": "^4.3.0",
"nock": "^8.0.0",
"node-sass": "^3.10.0",
"postcss-functions": "^2.1.0",
"postcss-loader": "^0.8.0",
"precss": "^1.4.0",
@ -49,6 +50,7 @@
"redux-mock-store": "^1.2.0",
"redux-thunk": "^2.1.0",
"require-hacker": "^2.1.4",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.0",
"stylus-loader": "1.5.1",
"url-loader": "^0.5.7",

View file

@ -6,8 +6,9 @@ var autoprefixer = require('autoprefixer');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var plugins = [
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new ExtractTextPlugin("bundle.css", {allChunks: false})
];
if (process.env.NODE_ENV === 'production') {
@ -39,6 +40,10 @@ var config = {
{test: /\.(png|gif)$/, loader: 'url-loader?name=[name]@[hash].[ext]&limit=6000'},
{test: /\.(pdf|ico|jpg|svg|eot|otf|woff|ttf|mp4|webm)$/, loader: 'file-loader?name=[name]@[hash].[ext]'},
{test: /\.json$/, loader: 'json-loader'},
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader!sass-loader")
},
{
test: /\.jsx?$/,
include: path.join(repo, 'frontend'),