Merge pull request #1244 from andrewn/feature/storybook

Storybook for component development
This commit is contained in:
Cassie Tarakajian 2020-05-26 16:21:30 -04:00 committed by GitHub
commit 1dcdfd39db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 8973 additions and 255 deletions

View file

@ -6,6 +6,7 @@
"env": {
"production": {
"plugins": [
"babel-plugin-styled-components",
"transform-react-remove-prop-types",
"@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-react-inline-elements",
@ -48,6 +49,7 @@
},
"development": {
"plugins": [
"babel-plugin-styled-components",
"react-hot-loader/babel"
]
}

View file

@ -77,5 +77,13 @@
"__SERVER__": true,
"__DISABLE_SSR__": true,
"__DEVTOOLS__": true
},
"overrides": [
{
"files": ["*.stories.jsx"],
"rules": {
"import/no-extraneous-dependencies": "off"
}
}
]
}

2
.gitignore vendored
View file

@ -17,3 +17,5 @@ cert_chain.crt
localhost.crt
localhost.key
privkey.pem
storybook-static

29
.storybook/main.js Normal file
View file

@ -0,0 +1,29 @@
const path = require('path');
module.exports = {
stories: ['../client/**/*.stories.(jsx|mdx)'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-docs',
'@storybook/addon-knobs',
'@storybook/addon-links',
'storybook-addon-theme-playground/dist/register'
],
webpackFinal: async config => {
// do mutation to the config
const rules = config.module.rules;
// modify storybook's file-loader rule to avoid conflicts with svgr
const fileLoaderRule = rules.find(rule => rule.test.test('.svg'));
fileLoaderRule.exclude = path.resolve(__dirname, '../client');
// use svgr for svg files
rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
})
return config;
},
};

31
.storybook/preview.js Normal file
View file

@ -0,0 +1,31 @@
import React from 'react';
import { addDecorator, addParameters } from '@storybook/react';
import { withKnobs } from "@storybook/addon-knobs";
import { withThemePlayground } from 'storybook-addon-theme-playground';
import { ThemeProvider } from "styled-components";
import theme, { Theme } from '../client/theme';
addDecorator(withKnobs);
const themeConfigs = Object.values(Theme).map(
name => {
return { name, theme: theme[name] };
}
);
addDecorator(withThemePlayground({
theme: themeConfigs,
provider: ThemeProvider
}));
addParameters({
options: {
/**
* display the top-level grouping as a "root" in the sidebar
*/
showRoots: true,
},
})
// addDecorator(storyFn => <ThemeProvider theme={theme}>{storyFn()}</ThemeProvider>);

240
client/common/Button.jsx Normal file
View file

@ -0,0 +1,240 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Link } from 'react-router';
import { remSize, prop } from '../theme';
const kinds = {
block: 'block',
icon: 'icon',
inline: 'inline',
};
// The '&&&' will increase the specificity of the
// component's CSS so that it overrides the more
// general global styles
const StyledButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
width: max-content;
text-decoration: none;
color: ${prop('Button.default.foreground')};
background-color: ${prop('Button.default.background')};
cursor: pointer;
border: 2px solid ${prop('Button.default.border')};
border-radius: 2px;
padding: ${remSize(8)} ${remSize(25)};
line-height: 1;
svg * {
fill: ${prop('Button.default.foreground')};
}
&:hover:not(:disabled) {
color: ${prop('Button.hover.foreground')};
background-color: ${prop('Button.hover.background')};
border-color: ${prop('Button.hover.border')};
svg * {
fill: ${prop('Button.hover.foreground')};
}
}
&:active:not(:disabled) {
color: ${prop('Button.active.foreground')};
background-color: ${prop('Button.active.background')};
svg * {
fill: ${prop('Button.active.foreground')};
}
}
&:disabled {
color: ${prop('Button.disabled.foreground')};
background-color: ${prop('Button.disabled.background')};
border-color: ${prop('Button.disabled.border')};
cursor: not-allowed;
svg * {
fill: ${prop('Button.disabled.foreground')};
}
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
const StyledInlineButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: ${prop('primaryTextColor')};
cursor: pointer;
border: none;
line-height: 1;
svg * {
fill: ${prop('primaryTextColor')};
}
&:disabled {
cursor: not-allowed;
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
const StyledIconButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
width: ${remSize(32)}px;
height: ${remSize(32)}px;
text-decoration: none;
color: ${prop('Button.default.foreground')};
background-color: ${prop('Button.hover.background')};
cursor: pointer;
border: 1px solid transparent;
border-radius: 50%;
padding: ${remSize(8)} ${remSize(25)};
line-height: 1;
&:hover:not(:disabled) {
color: ${prop('Button.hover.foreground')};
background-color: ${prop('Button.hover.background')};
svg * {
fill: ${prop('Button.hover.foreground')};
}
}
&:active:not(:disabled) {
color: ${prop('Button.active.foreground')};
background-color: ${prop('Button.active.background')};
svg * {
fill: ${prop('Button.active.foreground')};
}
}
&:disabled {
color: ${prop('Button.disabled.foreground')};
background-color: ${prop('Button.disabled.background')};
cursor: not-allowed;
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
/**
* A Button performs an primary action
*/
const Button = ({
children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props
}) => {
const hasChildren = React.Children.count(children) > 0;
const content = <>{iconBefore}{hasChildren && <span>{children}</span>}{iconAfter}</>;
let StyledComponent = StyledButton;
if (kind === kinds.inline) {
StyledComponent = StyledInlineButton;
} else if (kind === kinds.icon) {
StyledComponent = StyledIconButton;
}
if (href) {
return (
<StyledComponent
kind={kind}
as="a"
aria-label={ariaLabel}
href={href}
{...props}
>
{content}
</StyledComponent>
);
}
if (to) {
return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>;
}
return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>;
};
Button.defaultProps = {
'children': null,
'disabled': false,
'iconAfter': null,
'iconBefore': null,
'kind': kinds.block,
'href': null,
'aria-label': null,
'to': null,
'type': 'button',
};
Button.kinds = kinds;
Button.propTypes = {
/**
* The visible part of the button, telling the user what
* the action is
*/
'children': PropTypes.element,
/**
If the button can be activated or not
*/
'disabled': PropTypes.bool,
/**
* SVG icon to place after child content
*/
'iconAfter': PropTypes.element,
/**
* SVG icon to place before child content
*/
'iconBefore': PropTypes.element,
/**
* The kind of button - determines how it appears visually
*/
'kind': PropTypes.oneOf(Object.values(kinds)),
/**
* Specifying an href will use an <a> to link to the URL
*/
'href': PropTypes.string,
/*
* An ARIA Label used for accessibility
*/
'aria-label': PropTypes.string,
/**
* Specifying a to URL will use a react-router Link
*/
'to': PropTypes.string,
/**
* If using a button, then type is defines the type of button
*/
'type': PropTypes.oneOf(['button', 'submit']),
};
export default Button;

View file

@ -0,0 +1,70 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import Button from './Button';
import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons';
export default {
title: 'Common/Button',
component: Button
};
export const AllFeatures = () => (
<Button
disabled={boolean('disabled', false)}
type="submit"
label={text('label', 'submit')}
>
{text('children', 'this is the button')}
</Button>
);
export const SubmitButton = () => (
<Button type="submit" label="submit">This is a submit button</Button>
);
export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>;
export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>;
export const AnchorButton = () => (
<Button href="http://p5js.org" label="submit">Actually an anchor</Button>
);
export const ReactRouterLink = () => (
<Button to="./somewhere" label="submit">Actually a Link</Button>
);
export const ButtonWithIconBefore = () => (
<Button
iconBefore={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const ButtonWithIconAfter = () => (
<Button
iconAfter={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const InlineButtonWithIconAfter = () => (
<Button
iconAfter={<DropdownArrowIcon />}
kind={Button.kinds.inline}
>
File name
</Button>
);
export const InlineIconOnlyButton = () => (
<Button
aria-label="Add to collection"
iconBefore={<PlusIcon />}
kind={Button.kinds.inline}
/>
);

51
client/common/Icons.jsx Normal file
View file

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import SortArrowUp from '../images/sort-arrow-up.svg';
import SortArrowDown from '../images/sort-arrow-down.svg';
import Github from '../images/github.svg';
import Google from '../images/google.svg';
import Plus from '../images/plus-icon.svg';
import Close from '../images/close.svg';
import DropdownArrow from '../images/down-filled-triangle.svg';
// HOC that adds the right web accessibility props
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
// could also give these a default size, color, etc. based on the theme
// Need to add size to these - like small icon, medium icon, large icon. etc.
function withLabel(SvgComponent) {
const Icon = (props) => {
const { 'aria-label': ariaLabel } = props;
if (ariaLabel) {
return (<SvgComponent
{...props}
aria-label={ariaLabel}
role="img"
focusable="false"
/>);
}
return (<SvgComponent
{...props}
aria-hidden
focusable="false"
/>);
};
Icon.propTypes = {
'aria-label': PropTypes.string
};
Icon.defaultProps = {
'aria-label': null
};
return Icon;
}
export const SortArrowUpIcon = withLabel(SortArrowUp);
export const SortArrowDownIcon = withLabel(SortArrowDown);
export const GithubIcon = withLabel(Github);
export const GoogleIcon = withLabel(Google);
export const PlusIcon = withLabel(Plus);
export const CloseIcon = withLabel(Close);
export const DropdownArrowIcon = withLabel(DropdownArrow);

View file

@ -0,0 +1,18 @@
import React from 'react';
import { select } from '@storybook/addon-knobs';
import * as icons from './icons';
export default {
title: 'Common/Icons',
component: icons
};
export const AllIcons = () => {
const names = Object.keys(icons);
const SelectedIcon = icons[select('name', names, names[0])];
return (
<SelectedIcon />
);
};

View file

@ -1,47 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#FBBB00;" d="M113.47,309.408L95.648,375.94l-65.139,1.378C11.042,341.211,0,299.9,0,256
c0-42.451,10.324-82.483,28.624-117.732h0.014l57.992,10.632l25.404,57.644c-5.317,15.501-8.215,32.141-8.215,49.456
C103.821,274.792,107.225,292.797,113.47,309.408z"/>
<path style="fill:#518EF8;" d="M507.527,208.176C510.467,223.662,512,239.655,512,256c0,18.328-1.927,36.206-5.598,53.451
c-12.462,58.683-45.025,109.925-90.134,146.187l-0.014-0.014l-73.044-3.727l-10.338-64.535
c29.932-17.554,53.324-45.025,65.646-77.911h-136.89V208.176h138.887L507.527,208.176L507.527,208.176z"/>
<path style="fill:#28B446;" d="M416.253,455.624l0.014,0.014C372.396,490.901,316.666,512,256,512
c-97.491,0-182.252-54.491-225.491-134.681l82.961-67.91c21.619,57.698,77.278,98.771,142.53,98.771
c28.047,0,54.323-7.582,76.87-20.818L416.253,455.624z"/>
<path style="fill:#F14336;" d="M419.404,58.936l-82.933,67.896c-23.335-14.586-50.919-23.012-80.471-23.012
c-66.729,0-123.429,42.957-143.965,102.724l-83.397-68.276h-0.014C71.23,56.123,157.06,0,256,0
C318.115,0,375.068,22.126,419.404,58.936z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<path d="M7.091875,19.338 L5.978,23.49625 L1.9068125,23.582375 C0.690125,21.3256875 0,18.74375 0,16 C0,13.3468125 0.64525,10.8448125 1.789,8.64175 L1.789875,8.64175 L5.414375,9.30625 L7.002125,12.909 C6.6698125,13.8778125 6.4886875,14.9178125 6.4886875,16 C6.4888125,17.1745 6.7015625,18.2998125 7.091875,19.338 Z" fill="#FBBB00"></path>
<path d="M31.7204375,13.011 C31.9041875,13.978875 32,14.9784375 32,16 C32,17.1455 31.8795625,18.262875 31.650125,19.3406875 C30.87125,23.008375 28.8360625,26.211 26.01675,28.477375 L26.015875,28.4765 L21.450625,28.2435625 L20.8045,24.210125 C22.67525,23.113 24.13725,21.3960625 24.907375,19.3406875 L16.35175,19.3406875 L16.35175,13.011 L25.0321875,13.011 L31.7204375,13.011 Z" fill="#518EF8"></path>
<path d="M26.0158125,28.4765 L26.0166875,28.477375 C23.27475,30.6813125 19.791625,32 16,32 C9.9068125,32 4.60925,28.5943125 1.9068125,23.5824375 L7.091875,19.3380625 C8.4430625,22.9441875 11.92175,25.51125 16,25.51125 C17.7529375,25.51125 19.3951875,25.037375 20.804375,24.210125 L26.0158125,28.4765 Z" fill="#28B446"></path>
<path d="M26.21275,3.6835 L21.0294375,7.927 C19.571,7.015375 17.847,6.48875 16,6.48875 C11.8294375,6.48875 8.2856875,9.1735625 7.0021875,12.909 L1.789875,8.64175 L1.789,8.64175 C4.451875,3.5076875 9.81625,0 16,0 C19.8821875,0 23.44175,1.382875 26.21275,3.6835 Z" fill="#F14336"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/>
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(0.666667, 0.333333)">
<polygon points="4.56711429 7.96345714 0.103114286 0.1872 8.99022857 0.1872"></polygon>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 539 B

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/>
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(-0.200000, -0.200000)" >
<polygon points="4.93361111 0.202222222 9.75583333 8.6025 0.155833333 8.6025"></polygon>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 543 B

View file

@ -3,8 +3,10 @@ import { render } from 'react-dom';
import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import configureStore from './store';
import routes from './routes';
import ThemeProvider from './modules/App/components/ThemeProvider';
require('./styles/main.scss');
@ -18,7 +20,9 @@ const store = configureStore(initialState);
const App = () => (
<Provider store={store}>
<ThemeProvider>
<Router history={history} routes={routes(store)} />
</ThemeProvider>
</Provider>
);

7
client/index.stories.mdx Normal file
View file

@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title=" |Intro" />
# Welcome to the P5.js Web Editor Style Guide
This guide will contain all the components in the project, with examples of how they can be reused.

View file

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import theme, { Theme } from '../../../theme';
const Provider = ({ children, currentTheme }) => (
<ThemeProvider theme={{ ...theme[currentTheme] }}>
{children}
</ThemeProvider>
);
Provider.propTypes = {
children: PropTypes.node.isRequired,
currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired,
};
function mapStateToProps(state) {
return {
currentTheme: state.preferences.theme,
};
}
export default connect(mapStateToProps)(Provider);

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
class NewFileForm extends React.Component {
constructor(props) {
super(props);
@ -33,12 +35,10 @@ class NewFileForm extends React.Component {
{...domOnlyProps(name)}
ref={(element) => { this.fileName = element; }}
/>
<input
<Button
type="submit"
value="Add File"
aria-label="add file"
className="new-file-form__submit"
/>
>Add File
</Button>
</div>
{name.touched && name.error && <span className="form-error">{name.error}</span>}
</form>

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
class NewFolderForm extends React.Component {
constructor(props) {
super(props);
@ -34,12 +36,10 @@ class NewFolderForm extends React.Component {
ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)}
/>
<input
<Button
type="submit"
value="Add Folder"
aria-label="add folder"
className="new-folder-form__submit"
/>
>Add Folder
</Button>
</div>
{name.touched && name.error && <span className="form-error">{name.error}</span>}
</form>

View file

@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from '../../../common/Button';
import { PlusIcon } from '../../../common/icons';
import CopyableInput from '../../IDE/components/CopyableInput';
import APIKeyList from './APIKeyList';
import PlusIcon from '../../../images/plus-icon.svg';
export const APIKeyPropType = PropTypes.shape({
id: PropTypes.object.isRequired,
token: PropTypes.object,
@ -80,14 +80,14 @@ class APIKeyForm extends React.Component {
type="text"
value={this.state.keyLabel}
/>
<button
className="api-key-form__create-button"
<Button
disabled={this.state.keyLabel === ''}
iconBefore={<PlusIcon />}
label="Create new key"
type="submit"
>
<PlusIcon className="api-key-form__create-icon" focusable="false" aria-hidden="true" />
Create
</button>
</Button>
</form>
{

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function AccountForm(props) {
const {
@ -44,11 +45,10 @@ function AccountForm(props) {
<span className="form__status"> Confirmation sent, check your email.</span>
) :
(
<button
className="form__action"
<Button
onClick={handleInitiateVerification}
>Resend confirmation email
</button>
</Button>
)
}
</p>
@ -92,12 +92,11 @@ function AccountForm(props) {
/>
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
</p>
<input
<Button
type="submit"
disabled={submitting || invalid || pristine}
value="Save All Settings"
aria-label="updateSettings"
/>
>Save All Settings
</Button>
</form>
);
}

View file

@ -6,6 +6,9 @@ import { connect } from 'react-redux';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
import classNames from 'classnames';
import Button from '../../../common/Button';
import { DropdownArrowIcon } from '../../../common/icons';
import * as ProjectActions from '../../IDE/actions/project';
import * as ProjectsActions from '../../IDE/actions/projects';
import * as CollectionsActions from '../../IDE/actions/collections';
@ -20,7 +23,6 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc
import CopyableInput from '../../IDE/components/CopyableInput';
import { SketchSearchbar } from '../../IDE/components/Searchbar';
import DropdownArrowIcon from '../../../images/down-arrow.svg';
import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import RemoveIcon from '../../../images/close.svg';
@ -50,14 +52,12 @@ const ShareURL = ({ value }) => {
return (
<div className="collection-share" ref={node}>
<button
className="collection-share__button"
<Button
onClick={() => setShowURL(!showURL)}
aria-label="Show collection share URL"
iconAfter={<DropdownArrowIcon />}
>
<span>Share</span>
<DropdownArrowIcon className="collection-share__arrow" focusable="false" aria-hidden="true" />
</button>
Share
</Button>
{ showURL &&
<div className="collection__share-dropdown">
<CopyableInput value={value} label="Link to Collection" />
@ -264,9 +264,9 @@ class Collection extends React.Component {
</p>
{
this.isOwner() &&
<button className="collection-metadata__add-button" onClick={this.showAddSketches}>
<Button onClick={this.showAddSketches}>
Add Sketch
</button>
</Button>
}
</div>
</div>
@ -317,7 +317,7 @@ class Collection extends React.Component {
_renderFieldHeader(fieldName, displayName) {
const { field, direction } = this.props.sorting;
const headerClass = classNames({
'sketches-table__header': true,
'arrowDown': true,
'sketches-table__header--selected': field === fieldName
});
const buttonLabel = this._getButtonLabel(fieldName, displayName);

View file

@ -6,6 +6,7 @@ import { bindActionCreators } from 'redux';
import * as CollectionsActions from '../../IDE/actions/collections';
import { generateCollectionName } from '../../../utils/generateRandomName';
import Button from '../../../common/Button';
class CollectionCreate extends React.Component {
constructor() {
@ -81,7 +82,7 @@ class CollectionCreate extends React.Component {
rows="4"
/>
</p>
<input type="submit" disabled={invalid} value="Create collection" aria-label="create collection" />
<Button type="submit" disabled={invalid}>Create collection</Button>
</form>
</div>
</div>

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GithubIcon from '../../../images/github.svg';
function GithubButton(props) {
return (
<a
className="github-button"
href="/auth/github"
>
<GithubIcon className="github-icon" role="img" aria-label="GitHub Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GithubButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GithubButton;

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GoogleIcon from '../../../images/google.svg';
function GoogleButton(props) {
return (
<a
className="google-button"
href="/auth/google/"
>
<GoogleIcon className="google-icon" role="img" aria-label="Google Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GoogleButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GoogleButton;

View file

@ -1,5 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from '../../../common/Button';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
function LoginForm(props) {
@ -30,7 +33,11 @@ function LoginForm(props) {
/>
{password.touched && password.error && <span className="form-error">{password.error}</span>}
</p>
<input type="submit" disabled={submitting || pristine} value="Log In" aria-label="login" />
<Button
type="submit"
disabled={submitting || pristine}
>Log In
</Button>
</form>
);
}

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function NewPasswordForm(props) {
const {
@ -34,7 +36,7 @@ function NewPasswordForm(props) {
<span className="form-error">{confirmPassword.error}</span>
}
</p>
<input type="submit" disabled={submitting || invalid || pristine} value="Set New Password" aria-label="sign up" />
<Button type="submit" disabled={submitting || invalid || pristine}>Set New Password</Button>
</form>
);
}

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function ResetPasswordForm(props) {
const {
@ -19,12 +21,11 @@ function ResetPasswordForm(props) {
/>
{email.touched && email.error && <span className="form-error">{email.error}</span>}
</p>
<input
<Button
type="submit"
disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate}
value="Send Password Reset Email"
aria-label="Send email to reset password"
/>
>Send Password Reset Email
</Button>
</form>
);
}

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function SignupForm(props) {
const {
@ -58,7 +60,11 @@ function SignupForm(props) {
<span className="form-error">{confirmPassword.error}</span>
}
</p>
<input type="submit" disabled={submitting || invalid || pristine} value="Sign Up" aria-label="sign up" />
<Button
type="submit"
disabled={submitting || invalid || pristine}
>Sign Up
</Button>
</form>
);
}

View file

@ -0,0 +1,52 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import { remSize } from '../../../theme';
import { GithubIcon, GoogleIcon } from '../../../common/icons';
import Button from '../../../common/Button';
const authUrls = {
github: '/auth/github',
google: '/auth/google'
};
const labels = {
github: 'Login with GitHub',
google: 'Login with Google'
};
const icons = {
github: GithubIcon,
google: GoogleIcon
};
const services = {
github: 'github',
google: 'google'
};
const StyledButton = styled(Button)`
width: ${remSize(300)};
`;
function SocialAuthButton({ service }) {
const ServiceIcon = icons[service];
return (
<StyledButton
iconBefore={<ServiceIcon aria-label={`${service} logo`} />}
href={authUrls[service]}
>
{labels[service]}
</StyledButton>
);
}
SocialAuthButton.services = services;
SocialAuthButton.propTypes = {
service: PropTypes.oneOf(['github', 'google']).isRequired
};
export default SocialAuthButton;

View file

@ -0,0 +1,16 @@
import React from 'react';
import SocialAuthButton from './SocialAuthButton';
export default {
title: 'User/components/SocialAuthButton',
component: SocialAuthButton
};
export const Github = () => (
<SocialAuthButton service={SocialAuthButton.services.github}>Log in with Github</SocialAuthButton>
);
export const Google = () => (
<SocialAuthButton service={SocialAuthButton.services.google}>Sign up with Google</SocialAuthButton>
);

View file

@ -8,8 +8,7 @@ import { Helmet } from 'react-helmet';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import AccountForm from '../components/AccountForm';
import { validateSettings } from '../../../utils/reduxFormUtils';
import GithubButton from '../components/GithubButton';
import GoogleButton from '../components/GoogleButton';
import SocialAuthButton from '../components/SocialAuthButton';
import APIKeyForm from '../components/APIKeyForm';
import Nav from '../../../components/Nav';
@ -24,8 +23,10 @@ function SocialLoginPanel(props) {
<p className="account__social-text">
Use your GitHub or Google account to log into the p5.js Web Editor.
</p>
<GithubButton buttonText="Login with GitHub" />
<GoogleButton buttonText="Login with Google" />
<div className="account__social-stack">
<SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} />
</div>
</React.Fragment>
);
}

View file

@ -2,8 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { browserHistory, Link } from 'react-router';
import { browserHistory } from 'react-router';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import Button from '../../../common/Button';
import Nav from '../../../components/Nav';
import Overlay from '../../App/components/Overlay';
@ -79,16 +82,16 @@ class DashboardView extends React.Component {
case TabKey.collections:
return this.isOwner() && (
<React.Fragment>
<Link className="dashboard__action-button" to={`/${username}/collections/create`}>
<Button to={`/${username}/collections/create`}>
Create collection
</Link>
</Button>
<CollectionSearchbar />
</React.Fragment>);
case TabKey.sketches:
default:
return (
<React.Fragment>
{this.isOwner() && <Link className="dashboard__action-button" to="/">New sketch</Link>}
{this.isOwner() && <Button to="/">New sketch</Button>}
<SketchSearchbar />
</React.Fragment>
);

View file

@ -6,8 +6,7 @@ import { Helmet } from 'react-helmet';
import { validateAndLoginUser } from '../actions';
import LoginForm from '../components/LoginForm';
import { validateLogin } from '../../../utils/reduxFormUtils';
import GithubButton from '../components/GithubButton';
import GoogleButton from '../components/GoogleButton';
import SocialAuthButton from '../components/SocialAuthButton';
import Nav from '../../../components/Nav';
class LoginView extends React.Component {
@ -41,8 +40,10 @@ class LoginView extends React.Component {
<h2 className="form-container__title">Log In</h2>
<LoginForm {...this.props} />
<h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" />
<GoogleButton buttonText="Login with Google" />
<div className="form-container__stack">
<SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} />
</div>
<p className="form__navigation-options">
Don&apos;t have an account?&nbsp;
<Link className="form__signup-button" to="/signup">Sign Up</Link>

View file

@ -20,3 +20,12 @@
.account__social-text {
padding-bottom: #{15 / $base-font-size}rem;
}
.account__social-stack {
display: flex;
}
.account__social-stack > * {
margin-right: #{15 / $base-font-size}rem;
}

View file

@ -12,22 +12,6 @@
font-weight: bold;
}
.api-key-form__create-button {
display: flex;
justify-content: center;
align-items: center;
}
.api-key-form__create-icon {
display: flex;
height: #{12 / $base-font-size}rem;
margin-right: #{3 / $base-font-size}rem;
}
.api-key-form__create-button .isvg {
padding-right: 10px;
}
.api-key-list {
display: block;
max-width: 900px;

View file

@ -94,16 +94,6 @@
position: relative;
}
.collection-share__button {
@extend %button;
display: flex;
align-items: center;
}
.collection-share__arrow {
margin-left: #{5 / $base-font-size}rem;
}
.collection-share .copyable-input {
padding-bottom: 0;
}
@ -114,11 +104,6 @@
width: #{350 / $base-font-size}rem;
}
.collection-metadata__add-button {
@extend %button;
flex-grow: 0;
}
.collection-content {
display: flex;
flex-direction: column;

View file

@ -83,8 +83,3 @@
.dashboard-header__actions > *:not(:first-child) {
margin-left: #{15 / $base-font-size}rem;
}
.dashboard__action-button {
flex-grow: 0;
@extend %button;
}

View file

@ -57,3 +57,7 @@
.form-container__exit-button {
@include icon();
}
.form-container__stack > * + * {
margin-top: #{10 / $base-font-size}rem;
}

View file

@ -9,6 +9,10 @@
}
}
.form > * + * {
margin-top: #{12 / $base-font-size}rem;
}
.form--inline {
display: flex;
align-items: center;
@ -79,20 +83,25 @@
}
.form [type="submit"] {
@extend %button;
padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem;
margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
margin-left: auto;
margin-right: auto;
}
.form [type="submit"][disabled] {
cursor: not-allowed;
}
// .form [type="submit"] {
// @extend %button;
// padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem;
// margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
// }
.form--inline [type="submit"] {
margin: 0 0 0 #{24 / $base-font-size}rem;
}
// .form [type="submit"][disabled] {
// cursor: not-allowed;
// }
.form [type="submit"][disabled],
.form--inline [type="submit"][disabled] {
cursor: not-allowed;
}
// .form--inline [type="submit"] {
// margin: 0 0 0 #{24 / $base-font-size}rem;
// }
// .form [type="submit"][disabled],
// .form--inline [type="submit"][disabled] {
// cursor: not-allowed;
// }

View file

@ -1,35 +0,0 @@
.github-button,
.google-button {
@include themify() {
@extend %button;
& path {
color: getThemifyVariable('primary-text-color');
}
&:hover path, &:active path {
fill: $white;
}
&:hover, &:active {
color: getThemifyVariable('button-hover-color');
background-color: getThemifyVariable('button-background-hover-color');
border-color: getThemifyVariable('button-background-hover-color');
}
}
width: #{300 / $base-font-size}rem;
display: flex;
justify-content: center;
align-items: center;
& + & {
margin-top: #{10 / $base-font-size}rem;
}
}
.github-icon {
margin-right: #{10 / $base-font-size}rem;
}
.google-icon {
width: #{32 / $base-font-size}rem;
height: #{32 / $base-font-size}rem;
margin-right: #{10 / $base-font-size}rem;
}

View file

@ -29,6 +29,11 @@
display: flex;
align-items: center;
height: #{35 / $base-font-size}rem;
& .isvg {
margin-left: #{8 / $base-font-size}rem;
}
& svg {
@include themify() {
fill: getThemifyVariable('inactive-text-color')

View file

@ -31,7 +31,6 @@
@import 'components/resizer';
@import 'components/overlay';
@import 'components/about';
@import 'components/github-button';
@import 'components/forms';
@import 'components/toast';
@import 'components/timer';

145
client/theme.js Normal file
View file

@ -0,0 +1,145 @@
import lodash from 'lodash';
export const Theme = {
contrast: 'contrast',
dark: 'dark',
light: 'light',
};
export const colors = {
p5jsPink: '#ed225d',
processingBlue: '#007BBB',
p5jsActivePink: '#f10046',
white: '#fff',
black: '#000',
yellow: '#f5dc23',
orange: '#ffa500',
red: '#ff0000',
lightsteelblue: '#B0C4DE',
dodgerblue: '#1E90FF',
p5ContrastPink: ' #FFA9D9',
borderColor: ' #B5B5B5',
outlineColor: '#0F9DD7',
};
export const grays = {
lightest: '#FFF', // primary
lighter: '#FBFBFB',
light: '#F0F0F0', // primary
mediumLight: '#D9D9D9',
middleLight: '#A6A6A6',
middleGray: '#747474', // primary
middleDark: '#666',
mediumDark: '#4D4D4D',
dark: '#333', // primary
darker: '#1C1C1C',
darkest: '#000',
};
export const common = {
baseFontSize: 12
};
export const remSize = size => `${size / common.baseFontSize}rem`;
export const prop = key => (props) => {
const keypath = `theme.${key}`;
const value = lodash.get(props, keypath);
if (value == null) {
throw new Error(`themed prop ${key} not found`);
}
return value;
};
export default {
[Theme.light]: {
colors,
...common,
primaryTextColor: grays.dark,
Button: {
default: {
foreground: colors.black,
background: grays.light,
border: grays.middleLight,
},
hover: {
foreground: grays.lightest,
background: colors.p5jsPink,
border: colors.p5jsPink,
},
active: {
foreground: grays.lightest,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: colors.black,
background: grays.light,
border: grays.middleLight,
},
},
},
[Theme.dark]: {
colors,
...common,
primaryTextColor: grays.lightest,
Button: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
hover: {
foreground: grays.lightest,
background: colors.p5jsPink,
border: colors.p5jsPink,
},
active: {
foreground: grays.lightest,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
},
},
[Theme.contrast]: {
colors,
...common,
primaryTextColor: grays.lightest,
Button: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
hover: {
foreground: grays.dark,
background: colors.yellow,
border: colors.yellow,
},
active: {
foreground: grays.dark,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
},
},
};

8141
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,9 @@
"fetch-examples-gg:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-gg.bundle.js",
"fetch-examples-ml5:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-ml5.bundle.js",
"update-syntax-highlighting": "node ./server/scripts/update-syntax-highlighting.js",
"heroku-postbuild": "touch .env; npm run build"
"heroku-postbuild": "touch .env; npm run build",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"husky": {
"hooks": {
@ -69,6 +71,12 @@
"@babel/plugin-transform-react-inline-elements": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3",
"@storybook/addon-actions": "^5.3.6",
"@storybook/addon-docs": "^5.3.6",
"@storybook/addon-knobs": "^5.3.6",
"@storybook/addon-links": "^5.3.6",
"@storybook/addons": "^5.3.6",
"@storybook/react": "^5.3.6",
"@svgr/webpack": "^5.4.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^9.0.0",
@ -99,6 +107,7 @@
"react-test-renderer": "^16.12.0",
"rimraf": "^2.7.1",
"sass-loader": "^6.0.7",
"storybook-addon-theme-playground": "^1.2.0",
"style-loader": "^1.1.3",
"terser-webpack-plugin": "^1.4.3",
"webpack-cli": "^3.3.11",
@ -117,6 +126,7 @@
"archiver": "^1.1.0",
"async": "^2.6.3",
"axios": "^0.18.1",
"babel-plugin-styled-components": "^1.10.6",
"bcrypt-nodejs": "0.0.3",
"blob-util": "^1.2.1",
"body-parser": "^1.18.3",
@ -191,6 +201,8 @@
"sinon-mongoose": "^2.3.0",
"slugify": "^1.3.6",
"srcdoc-polyfill": "^0.2.0",
"styled-components": "^5.0.0",
"styled-theming": "^2.2.0",
"url": "^0.11.0",
"webpack": "^4.41.6",
"webpack-dev-middleware": "^2.0.6",