feat(layout): add base page layout (#24)

* feat(layout): add base page layout

Add base layout component with dark mode support

fix #15

* feat(storybook): add dark mode support to all components
This commit is contained in:
Rui Saraiva 2020-04-12 09:43:27 +01:00 committed by GitHub
parent a611bcd4fe
commit d3f0cfcb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 280 additions and 87 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1 +1 @@
<script src="/noflash.js"></script>
<script src="/js/noflash.js"></script>

View File

@ -1,4 +1,12 @@
.sb-show-main {
margin: 0;
background: inherit;
transition: background 0.2s ease;
}
.sb-show-main.light-mode {
background: #fff;
}
.sb-show-main.dark-mode {
background: #191a1f;
}

View File

@ -0,0 +1,16 @@
@import '../../styles/variables.scss';
.root {
padding: 0;
}
.container {
max-width: 1440px;
padding: 20px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DarkModeToggle } from 'components/dark-mode-toggle'
import styles from './index.module.scss'
import { Typography } from 'components/typography'
type BaseLayoutProps = {
children: PropTypes.ReactNodeLike
}
const BaseLayout = ({ children }: BaseLayoutProps) => {
return (
<div className={styles.root}>
<div className={styles.container}>
<header className={styles.header}>
<Typography weight="bold">Menu</Typography>
<DarkModeToggle />
</header>
</div>
<main>{children}</main>
</div>
)
}
export { BaseLayout }

View File

@ -5,10 +5,20 @@
font-size: $font-size-medium-body;
line-height: $line-height-medium-body;
color: $black;
cursor: pointer;
background-color: $off-white;
border: 1px solid $light-gray-one;
border-radius: 4px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12);
transition: all 0.2s ease;
&:focus {
outline: none;
}
}
.noClick {
cursor: default;
}
.transparent {
@ -23,6 +33,7 @@
.dark {
color: $white;
background-color: $dark-purple;
border: 1px solid $dark-purple;
}
.regular {

View File

@ -14,11 +14,13 @@ export const ButtonDesktop = () => {
const weight = select('weight', ['regular', 'medium', 'semiBold', 'bold'], 'bold')
const body = text('body', 'Mac app coming soon')
return (
<div>
<div style={{ padding: 20 }}>
<Button variant={variant} weight={weight}>
{body}
</Button>
<button onClick={() => toggle()}>Toggle Dark Mode</button>
<br />
<br />
<Button onClick={toggle}>Toggle Dark Mode</Button>
</div>
)
}

View File

@ -11,11 +11,23 @@ type ButtonProps = {
weight: 'regular' | 'medium' | 'semiBold' | 'bold'
}
const Button = ({ children, variant, weight }: ButtonProps) => {
const { value: isDark } = useDarkMode()
const className = classNames(styles[variant], { [styles.dark]: isDark }, styles[weight])
const Button = ({
children,
variant,
weight,
...props
}: ButtonProps & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>) => {
const { value: isDark } = useDarkMode(false)
const className = classNames(styles[variant], styles[weight], {
[styles.dark]: isDark,
[styles.noClick]: !props.onClick,
})
return <button className={className}>{children}</button>
return (
<button className={className} {...props}>
{children}
</button>
)
}
Button.defaultProps = {

View File

@ -1,21 +1,26 @@
@import '../../styles/variables.scss';
.root {
padding: 2px;
position: relative;
}
.label {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
width: 32px;
height: 32px;
cursor: pointer;
user-select: none;
}
.checkbox {
position: relative;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
cursor: pointer;
visibility: hidden;

View File

@ -1,6 +1,7 @@
import React from 'react'
import { DarkModeToggle } from '.'
import useDarkMode from 'use-dark-mode'
import { Typography } from 'components/typography'
export default {
title: 'Dark Mode Toggle',
@ -10,9 +11,10 @@ export const ToggleDesktop = () => {
const darkMode = useDarkMode(false)
return (
<div>
<div style={{ padding: 20 }}>
<DarkModeToggle />
<div>dark mode: {darkMode.value ? 'enabled' : 'disabled'}</div>
<br />
<Typography>dark mode: {darkMode.value ? 'enabled' : 'disabled'}</Typography>
</div>
)
}

View File

@ -10,9 +10,6 @@ const DarkModeToggle = () => {
return (
<div className={styles.root}>
<label htmlFor="darkMode" className={styles.label}>
{darkMode.value ? <SvgSun /> : <SvgMoon />}
</label>
<input
className={styles.checkbox}
type="checkbox"
@ -20,6 +17,9 @@ const DarkModeToggle = () => {
checked={darkMode.value}
onChange={darkMode.toggle}
/>
<label htmlFor="darkMode" className={styles.label}>
{darkMode.value ? <SvgSun /> : <SvgMoon />}
</label>
</div>
)
}

19
components/seo/index.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react'
import Head from 'next/head'
type SEOProps = {
title: string
description: string
}
const SEO = ({ title, description }: SEOProps) => (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
<link rel="icon" type="image/png" href="/favicon.png?v=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=1" />
</Head>
)
export { SEO }

View File

@ -5,7 +5,7 @@ const SvgMoon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>>
const idPrefix = uniqueId('svg-')
return (
<svg width={32} height={32} aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Moon</title>
<title id={`${idPrefix}-title`}>Dark mode</title>
<path
d="M24.613 22.62a9.999 9.999 0 11-8.287-16.602c.406-.03.79.189.975.552a1 1 0 01-.15 1.11 7.996 7.996 0 006.633 13.278 1 1 0 01.83 1.662z"
fill="#CCC"

View File

@ -5,7 +5,7 @@ const SvgSun: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>>
const idPrefix = uniqueId('svg-')
return (
<svg width={32} height={32} aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Sun</title>
<title id={`${idPrefix}-title`}>Light mode</title>
<path
d="M16 25a1 1 0 01.993.883L17 26v3a1 1 0 01-1.993.117L15 29v-3a1 1 0 011-1zm-6.422-2.578a1 1 0 01.13 1.255l-.078.103-2.12 2.12a1 1 0 11-1.5-1.324l.09-.086 2.12-2.13v.01a1 1 0 011.358.052zm14.099-.14l.103.078 2.12 2.12a1 1 0 01-1.307 1.487l-.103-.077-2.12-2.12a1 1 0 011.307-1.487zM16 8a8 8 0 110 16 8 8 0 010-16zm0 2a6 6 0 100 12 6 6 0 000-12zM6 15a1 1 0 01.117 1.993L6 17H3a1 1 0 01-.117-1.993L3 15h3zm23 0a1 1 0 01.117 1.993L29 17h-3a1 1 0 01-.117-1.993L26 15h3zM7.407 6.023l.103.077 2.13 2.12a1.004 1.004 0 01-1.325 1.504L8.22 9.64 6.1 7.51a1 1 0 011.307-1.487zm18.44.13a1 1 0 01.13 1.254l-.077.103-2.12 2.13a1 1 0 01-.71.29 1 1 0 01-.799-1.61l.089-.1 2.13-2.12a1 1 0 011.358.052zM16 2a1 1 0 01.993.883L17 3v3a1 1 0 01-1.993.117L15 6V3a1 1 0 011-1z"
fill="#CCC"

View File

@ -0,0 +1,6 @@
@import '../../styles/variables.scss';
.root {
display: flex;
align-items: flex-start;
}

View File

@ -0,0 +1,26 @@
import React from 'react'
import { withKnobs, text } from '@storybook/addon-knobs'
import useDarkMode from 'use-dark-mode'
import { Button } from 'components/button'
import { ToolTitle } from '.'
export default {
title: 'Tool Title',
decorators: [withKnobs],
}
export const ToolTitleDesktop = () => {
const { toggle } = useDarkMode(false)
const body = text('body', 'Favycon')
return (
<div style={{ padding: 20 }}>
<ToolTitle>{body}</ToolTitle>
<br />
<Button onClick={toggle}>Toggle Dark Mode</Button>
</div>
)
}
ToolTitleDesktop.story = {
name: 'desktop',
}

View File

@ -0,0 +1,21 @@
import React from 'react'
import { Typography } from 'components/typography'
import styles from './index.module.scss'
type ToolTitleProps = {
children: string
}
const ToolTitle = ({ children }: ToolTitleProps) => (
<div className={styles.root}>
<Typography variant="h1" weight="bold">
{children}
</Typography>
<Typography variant="superscript" tag="span" weight="bold" color="gray">
TOOL
</Typography>
</div>
)
export { ToolTitle }

View File

@ -53,6 +53,11 @@
.black {
color: $black;
transition: color 0.2s ease;
&.dark {
color: $white;
}
}
.gray {
@ -61,4 +66,9 @@
.white {
color: $white;
transition: color 0.2s ease;
&.dark {
color: $black;
}
}

View File

@ -1,5 +1,7 @@
import React from 'react'
import { withKnobs, text, select } from '@storybook/addon-knobs'
import useDarkMode from 'use-dark-mode'
import { Button } from 'components/button'
import { Typography } from '.'
export default {
@ -7,7 +9,8 @@ export default {
decorators: [withKnobs],
}
export const typographyDesktop = () => {
export const TypographyDesktop = () => {
const { toggle } = useDarkMode(false)
const variant = select(
'variant',
['h1', 'smallBody', 'regularBody', 'mediumBody', 'largeBody', 'footer', 'superscript'],
@ -17,12 +20,16 @@ export const typographyDesktop = () => {
const color = select('color', ['black', 'gray', 'white'], 'black')
const body = text('body', 'Some text')
return (
<Typography variant={variant} weight={weight} color={color}>
{body}
</Typography>
<div style={{ padding: 20 }}>
<Typography variant={variant} weight={weight} color={color}>
{body}
</Typography>
<br />
<Button onClick={toggle}>Toggle Dark Mode</Button>
</div>
)
}
typographyDesktop.story = {
TypographyDesktop.story = {
name: 'Desktop',
}

View File

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import useDarkMode from 'use-dark-mode'
import styles from './index.module.scss'
@ -13,7 +14,8 @@ type TypographyProps = {
}
const Typography = ({ children, variant, weight, color, tag }: TypographyProps) => {
const className = classNames(styles[variant], styles[color], styles[weight])
const { value: isDark } = useDarkMode(false)
const className = classNames(styles[variant], styles[color], styles[weight], { [styles.dark]: isDark })
const componentType = ['h1'].includes(variant) ? variant : 'p'
return React.createElement(tag || componentType, { className }, children)

View File

@ -7,7 +7,7 @@ class MyDocument extends Document {
<Html>
<Head />
<body>
<script src="/noflash.js" />
<script src="/js/noflash.js" />
<Main />
<NextScript />
</body>

View File

@ -4,8 +4,8 @@ import { convert } from '@fiahfy/ico-convert'
import multer from 'multer'
import AdmZip from 'adm-zip'
import { runMiddleware } from '../../utils/api'
import { faviconSizesPrefixes, browserConfigTemplate, tutorialTemplate } from '../../utils/favicon'
import { runMiddleware } from 'utils/api'
import { faviconSizesPrefixes, browserConfigTemplate, tutorialTemplate } from 'utils/favicon'
const storage = multer.memoryStorage()
const upload = multer({ storage })

27
pages/appycon.tsx Normal file
View File

@ -0,0 +1,27 @@
import React from 'react'
import { NextPage } from 'next'
import { BaseLayout } from 'components/base-layout'
import { Typography } from 'components/typography'
import { SEO } from 'components/seo'
import { ToolTitle } from 'components/tool-title'
const Appycon: NextPage = () => (
<BaseLayout>
<SEO title="Appycon" description="Appycon page description" />
<main>
<ToolTitle>Appycon</ToolTitle>
<Typography variant="largeBody" weight="medium">
A small online tool to help you generate your favicon in all the sizes and formats you need. <br />
<br />
Just drag &amp; drop an image and you will then get a downloadable file alongside some documentation on how to
add the favicons.
</Typography>
<hr />
<Typography variant="footer" weight="semiBold" color="gray">
Created by <u>4 people</u> on their 2020s worldwide quarantine. Background image from <u>Unsplash</u>.
</Typography>
</main>
</BaseLayout>
)
export default Appycon

30
pages/favycon.tsx Normal file
View File

@ -0,0 +1,30 @@
import React from 'react'
import { NextPage } from 'next'
import { BaseLayout } from 'components/base-layout'
import { Typography } from 'components/typography'
import { SEO } from 'components/seo'
import { ToolTitle } from 'components/tool-title'
const Favycon: NextPage = () => (
<BaseLayout>
<SEO
title="Favycon"
description="A small online tool to help you generate your favicon in all the sizes and formats you need."
/>
<main>
<ToolTitle>Favycon</ToolTitle>
<Typography variant="largeBody" weight="medium">
A small online tool to help you generate your favicon in all the sizes and formats you need. <br />
<br />
Just drag &amp; drop an image and you will then get a downloadable file alongside some documentation on how to
add the favicons.
</Typography>
<hr />
<Typography variant="footer" weight="semiBold" color="gray">
Created by <u>4 people</u> on their 2020s worldwide quarantine. Background image from <u>Unsplash</u>.
</Typography>
</main>
</BaseLayout>
)
export default Favycon

View File

@ -1,65 +1,17 @@
import React from 'react'
import { NextPage } from 'next'
import Head from 'next/head'
import { Typography } from '../components/typography'
import { BaseLayout } from 'components/base-layout'
import { SEO } from 'components/seo'
import { ToolTitle } from 'components/tool-title'
const Home: NextPage = () => (
<div className="container">
<Head>
<title>Favycon</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
<link rel="icon" type="image/png" href="/favicon.png?v=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=1" />
</Head>
<BaseLayout>
<SEO title="Home" description="Home page description" />
<main>
<div className="title">
<Typography variant="h1" weight="bold">
Favycon
</Typography>
<Typography variant="superscript" tag="span" weight="bold" color="gray">
TOOL
</Typography>
</div>
<Typography variant="largeBody" weight="medium">
A small online tool to help you generate your favicon in all the sizes and formats you need. <br />
<br />
Just drag &amp; drop an image and you will then get a downloadable file alongside some documentation on how to
add the favicons.
</Typography>
<hr />
<Typography variant="footer" weight="semiBold" color="gray">
Created by <u>4 people</u> on their 2020s worldwide quarantine. Background image from <u>Unsplash</u>.
</Typography>
<ToolTitle>Favycon</ToolTitle>
<ToolTitle>Appycon</ToolTitle>
</main>
<style jsx>
{`
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.title {
display: flex;
align-items: flex-start;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
max-width: 350px;
}
`}
</style>
</div>
</BaseLayout>
)
export default Home

View File

@ -15,6 +15,15 @@ body {
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
transition: background 0.2s ease;
&.light-mode {
background: $white;
}
&.dark-mode {
background: $off-black;
}
}
* {

View File

@ -1,13 +1,14 @@
$white: #fff;
$off-white: #fafafa;
$gray: #8f8f8f;
$gray: #9698a3;
$dar-gray: #2f303a;
$light-gray-one: #e5e5e5;
$light-gray-two: #ebebeb;
$light-gray-three: #ccc;
$black: #000;
$off-black: #191a1f;
$blue: #4c66d5;
$light-blue: #e7ecff;
@ -15,8 +16,9 @@ $light-blue: #e7ecff;
$orange: #d5824c;
$light-orange: #fff0e7;
$dark-purple: #363743;
$light-purple: #4f5263;
$dark-purple: #33353d;
$light-purple-one: #3d3f47;
$light-purple-two: #5a5b66;
$font-size-h1: 52px;
$line-height-h1: 64px;

View File

@ -27,5 +27,5 @@
}
},
"exclude": ["node_modules"],
"include": ["**/*.ts", "**/*.tsx", "./*.js", "./public/*.js"]
"include": ["**/*.ts", "**/*.tsx", "./*.js", "./public/**/*.js"]
}