diff --git a/README.md b/README.md index 814bf46..710a351 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Layout scaffolding for Web apps. +This library is made to avoid custom repetitive layout code. + ## Usage Just import: @@ -49,4 +51,21 @@ const Page: React.FC = ({ export default Page ``` -The available props per layout is included as a TypeScript declarations file. +The available props per layout are included as a TypeScript declarations file. + +## Configuration + +There are CSS variables that can be declared in the parent of the `*.Layout` components +(preferably `:root`) for customizing the metrics and colors of the layout: + +### `--height-topbar` + +Default value: `4rem` + +Height of the top bar widget. + +### `--size-menu` + +Default value: `4rem` + +Width or height of the menu, depending on the orientation it is rendered. diff --git a/package.json b/package.json index cbcc609..2c514f9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..9fe1c9e --- /dev/null +++ b/src/config.json @@ -0,0 +1,3 @@ +{ + "base-width": 360 +} diff --git a/src/layouts/Basic/index.tsx b/src/layouts/Basic/index.tsx index 700c77c..f174820 100644 --- a/src/layouts/Basic/index.tsx +++ b/src/layouts/Basic/index.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import styled from 'styled-components'; -import TopBar from '../../widgets/TopBar'; +import styled, { createGlobalStyle } from 'styled-components' +import TopBar from '../../widgets/TopBar' +import {configVar, loadConfig} from '../../utilities/helpers' -const LayoutBase = styled('div')({ - '--width-base': 'var(--width-base, 360px)', - '--height-topbar': 'var(--height-topbar, 4rem)', +const Config = createGlobalStyle({ + ...loadConfig(), }) const ContentBase = styled('main')({ @@ -15,7 +15,7 @@ export const ContentContainer = styled('div')({ padding: '0 1rem', boxSizing: 'border-box', margin: '0 auto', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, width: '100%', }) @@ -32,7 +32,8 @@ export const Layout: React.FC = ({ children, }) => { return ( - + <> + = ({ {children} - + ) } diff --git a/src/layouts/LeftSidebar/index.tsx b/src/layouts/LeftSidebar/index.tsx index d2a7f35..824ce55 100644 --- a/src/layouts/LeftSidebar/index.tsx +++ b/src/layouts/LeftSidebar/index.tsx @@ -1,26 +1,27 @@ -import * as React from 'react'; -import styled, {createGlobalStyle} from 'styled-components'; -import TopBar from '../../widgets/TopBar'; +import * as React from 'react' +import styled, {createGlobalStyle} from 'styled-components' +import TopBar from '../../widgets/TopBar' +import {applyBackgroundColor, minWidthFactor} from '../../utilities/mixins' +import {configVar, loadConfig} from '../../utilities/helpers' + +const Config = createGlobalStyle({ + ...loadConfig(), +}) const DisableScrolling = createGlobalStyle({ 'body': { overflow: 'hidden', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ overflow: 'auto', - }, + }), }, }) -const LayoutBase = styled('div')({ - '--width-base': 'var(--width-base, 360px)', - '--height-topbar': 'var(--height-topbar, 4rem)', -}) - const ContentBase = styled('main')({ boxSizing: 'border-box', - '@media (min-width: 1080px)': { - paddingLeft: 'calc(50% - var(--width-base, 360px) * 0.5)', - }, + ...minWidthFactor(3)({ + paddingLeft: `calc(50% - ${configVar('base-width')} * 0.5)`, + }), }) const SidebarOverflow = styled('div')({ @@ -41,15 +42,12 @@ const SidebarBase = styled('div')({ left: '-100%', width: '100%', height: '100%', - backgroundColor: 'var(--color-bg, white)', overflow: 'hidden', - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - '@media (min-width: 1080px)': { - width: 'calc(50% - var(--width-base, 360px) * 0.5)', + ...applyBackgroundColor(), + ...minWidthFactor(3)({ + width: `calc(50% - ${configVar('base-width')} * 0.5)`, left: 0, - }, + }), }) const OpenSidebarBase = styled(SidebarBase)({ @@ -60,11 +58,11 @@ export const SidebarContainer = styled('div')({ margin: '0 auto', padding: '0 1rem', boxSizing: 'border-box', - maxWidth: 'calc(var(--width-base, 360px) * 2)', - '@media (min-width: 1080px)': { - width: 'var(--width-base, 360px)', + maxWidth: `calc(${configVar('base-width')} * 2)`, + ...minWidthFactor(3)({ + width: `${configVar('base-width')}`, marginRight: 0, - }, + }), }) export const ContentContainer = styled('div')({ @@ -72,12 +70,12 @@ export const ContentContainer = styled('div')({ boxSizing: 'border-box', width: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, marginRight: 'auto', marginLeft: 'auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ marginLeft: 0, - }, + }), }) type Props = { @@ -101,7 +99,8 @@ export const Layout: React.FC = ({ const LeftSidebarComponent = sidebarMainOpen ? OpenSidebarBase : SidebarBase return ( - + <> + { sidebarMainOpen && ( @@ -124,6 +123,6 @@ export const Layout: React.FC = ({ {children} - + ) } diff --git a/src/layouts/LeftSidebarWithMenu/index.tsx b/src/layouts/LeftSidebarWithMenu/index.tsx index c2f7705..78bb516 100644 --- a/src/layouts/LeftSidebarWithMenu/index.tsx +++ b/src/layouts/LeftSidebarWithMenu/index.tsx @@ -1,52 +1,48 @@ import * as React from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; -import TopBar from '../../widgets/TopBar'; +import styled, { createGlobalStyle } from 'styled-components' +import TopBar from '../../widgets/TopBar' +import {applyBackgroundColor, minWidthFactor} from '../../utilities/mixins' +import {configVar, loadConfig} from '../../utilities/helpers' + +const Config = createGlobalStyle({ + ...loadConfig(), +}) const DisableScrolling = createGlobalStyle({ 'body': { overflow: 'hidden', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ overflow: 'auto', - }, + }), }, }) -const LayoutBase = styled('div')({ - '--width-base': 'var(--width-base, 360px)', - '--height-topbar': 'var(--height-topbar, 4rem)', - '--size-menu': 'var(--size-menu, 4rem)', -}) - const ContentBase = styled('main')({ boxSizing: 'border-box', paddingBottom: 'var(--size-menu, 4rem)', - '@media (min-width: 1080px)': { - paddingLeft: 'calc(50% - var(--width-base, 360px) * 0.5)', + ...minWidthFactor(3)({ + paddingLeft: `calc(50% - ${configVar('base-width')} * 0.5)`, paddingBottom: 0, - }, + }), }) const SidebarBase = styled('div')({ boxSizing: 'border-box', - backgroundColor: 'var(--color-bg, white)', overflow: 'hidden', display: 'contents', - left: 'calc(var(--width-base, 360px) * -1)', - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - '@media (min-width: 1080px)': { + left: `calc(${configVar('base-width')} * -1)`, + ...applyBackgroundColor(), + ...minWidthFactor(3)({ position: 'fixed', top: 0, left: 0, - width: 'calc(50% - var(--width-base, 360px) * 0.5)', + width: `calc(50% - ${configVar('base-width')} * 0.5)`, height: '100%', display: 'block', - }, + }), }) const SidebarMain = styled('div')({ - backgroundColor: 'var(--color-bg, white)', boxSizing: 'border-box', position: 'fixed', top: 0, @@ -57,20 +53,18 @@ const SidebarMain = styled('div')({ // overflow: 'overlay', paddingTop: 'inherit', paddingBottom: 'var(--size-menu, 4rem)', - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - scrollbarWidth: 'none', + scrollbarWidth: 'none', '::-webkit-scrollbar': { display: 'none', }, - '@media (min-width: 1080px)': { + ...applyBackgroundColor(), + ...minWidthFactor(3)({ position: 'absolute', right: 0, - width: 'calc(var(--width-base, 360px) - var(--size-menu, 4rem))', + width: `calc(${configVar('base-width')} - var(--size-menu, 4rem))`, marginLeft: 'auto', paddingBottom: 0, - }, + }), }) const OpenSidebarMain = styled(SidebarMain)({ @@ -91,11 +85,8 @@ const SidebarMenu = styled('div')({ width: '100%', height: 'var(--size-menu, 4rem)', zIndex: 1, - backgroundColor: 'var(--color-bg, white)', - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - '@media (min-width: 1080px)': { + ...applyBackgroundColor(), + ...minWidthFactor(3)({ top: 0, marginLeft: 'auto', position: 'absolute', @@ -103,30 +94,30 @@ const SidebarMenu = styled('div')({ paddingTop: 'inherit', overflow: 'auto', zIndex: 'auto', - }, + }), }) const SidebarMenuSize = styled('div')({ display: 'flex', width: '100%', height: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, margin: '0 auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ maxWidth: 'none', marginRight: 0, flexDirection: 'column', justifyContent: 'space-between', alignItems: 'flex-end', - }, + }), }) const SidebarMenuGroup = styled('div')({ display: 'contents', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ width: '100%', display: 'block', - }, + }), }) const MoreItems = styled('div')({ @@ -137,15 +128,12 @@ const MoreItems = styled('div')({ height: '100%', paddingTop: 'var(--height-topbar, 4rem)', paddingBottom: 'var(--size-menu, 4rem)', - backgroundColor: 'var(--color-bg, white)', zIndex: -1, boxSizing: 'border-box', - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - '@media (min-width: 1080px)': { + ...applyBackgroundColor(), + ...minWidthFactor(3)({ display: 'contents', - }, + }), }) const OpenMoreItems = styled(MoreItems)({ @@ -156,21 +144,21 @@ const MoreItemsScroll = styled('div')({ width: '100%', height: '100%', overflow: 'auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ display: 'contents', - }, + }), }) const MorePrimarySidebarMenuGroup = styled(SidebarMenuGroup)({ - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ flex: 'auto', - }, + }), }) const MoreSecondarySidebarMenuGroup = styled(SidebarMenuGroup)({ - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ order: 4, - }, + }), }) const SidebarMenuItem = styled('span')({ @@ -184,13 +172,13 @@ const SidebarMenuItem = styled('span')({ textDecoration: 'none', width: '100%', }, - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ width: 'auto !important', flex: '0 1 auto', '> *': { height: 'auto', - } - }, + }, + }), }) const MoreSidebarMenuItem = styled('span')({ @@ -203,38 +191,38 @@ const MoreSidebarMenuItem = styled('span')({ textDecoration: 'none', width: '100%', }, - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ width: 'auto !important', flex: '0 1 auto', - }, + }), }) const MoreToggleSidebarMenuItem = styled(SidebarMenuItem)({ - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ display: 'none', - }, + }), }) export const SidebarMenuItemIcon = styled('span')({ display: 'block', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ width: 'var(--size-menu, 4rem)', height: 'var(--size-menu, 4rem)', display: 'grid', placeContent: 'center', - }, + }), }) export const MoreSidebarMenuItemIcon = styled('span')({ marginRight: '1rem', display: 'block', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ width: 'var(--size-menu, 4rem)', height: 'var(--size-menu, 4rem)', display: 'grid', placeContent: 'center', marginRight: 0, - }, + }), }) export const SidebarMenuContainer = styled('span')({ @@ -243,32 +231,32 @@ export const SidebarMenuContainer = styled('span')({ placeContent: 'center', width: '100%', textAlign: 'center', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', - width: 'var(--width-base, 360px)', + width: `${configVar('base-width')}`, marginLeft: 'auto', paddingRight: '1rem', textAlign: 'left', boxSizing: 'border-box', - }, + }), }) export const MoreSidebarMenuContainer = styled('div')({ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', - width: 'calc(var(--width-base, 360px) * 2)', + width: `calc(${configVar('base-width')} * 2)`, margin: '0 auto', padding: '0 1rem', textAlign: 'left', boxSizing: 'border-box', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ marginRight: 0, - width: 'var(--width-base, 360px)', + width: `${configVar('base-width')}`, paddingLeft: 0, - }, + }), }) export const ContentContainer = styled('div')({ @@ -276,23 +264,23 @@ export const ContentContainer = styled('div')({ boxSizing: 'border-box', width: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, marginRight: 'auto', marginLeft: 'auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ marginLeft: 0, - }, + }), }) export const SidebarMainContainer = styled('div')({ padding: '0 1rem', boxSizing: 'border-box', width: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, margin: '0 auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ maxWidth: 'none', - }, + }), }) type BaseMenuItem = { @@ -354,13 +342,14 @@ export const Layout: React.FC = ({ return ( <> + { (sidebarMainOpen || moreItemsOpen) && ( ) } - + <> = ({ {children} - + ) } diff --git a/src/layouts/RightSidebarStatic/index.tsx b/src/layouts/RightSidebarStatic/index.tsx index d43fdfe..cef23e8 100644 --- a/src/layouts/RightSidebarStatic/index.tsx +++ b/src/layouts/RightSidebarStatic/index.tsx @@ -1,22 +1,22 @@ -import * as React from 'react'; -import styled from 'styled-components'; -import TopBar from '../../widgets/TopBar'; +import * as React from 'react' +import styled, {createGlobalStyle} from 'styled-components'; +import TopBar from '../../widgets/TopBar' +import {applyBackgroundColor, minWidthFactor} from '../../utilities/mixins' +import {configVar, loadConfig} from '../../utilities/helpers' -const LayoutBase = styled('div')({ - '--width-base': 'var(--width-base, 360px)', - '--height-topbar': 'var(--height-topbar, 4rem)', +const Config = createGlobalStyle({ + ...loadConfig(), }) const ContentBase = styled('main')({ boxSizing: 'border-box', - '@media (min-width: 1080px)': { - paddingRight: 'calc(50% - var(--width-base, 360px) * 0.5)', - }, + ...minWidthFactor(3)({ + paddingRight: `calc(50% - ${configVar('base-width')} * 0.5)`, + }), }) const SidebarBase = styled('div')({ boxSizing: 'border-box', - backgroundColor: 'var(--color-bg, white)', // prevent collapse of margin '::after': { content: "''", @@ -25,39 +25,37 @@ const SidebarBase = styled('div')({ marginTop: -1, boxSizing: 'border-box', }, - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, - '@media (min-width: 1080px)': { + ...applyBackgroundColor(), + ...minWidthFactor(3)({ position: 'absolute', top: 0, right: 0, - width: 'calc(50% - var(--width-base, 360px) * 0.5)', + width: `calc(50% - ${configVar('base-width')} * 0.5)`, height: '100%', - }, + }), }) export const SidebarContainer = styled('div')({ padding: '0 1rem', boxSizing: 'border-box', width: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, margin: '0 auto', - '@media (min-width: 1080px)': { - width: 'var(--width-base, 360px)', + ...minWidthFactor(3)({ + width: `${configVar('base-width')}`, marginLeft: 0, - }, + }), }) export const ContentContainer = styled('div')({ padding: '0 1rem', boxSizing: 'border-box', width: '100%', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, margin: '0 auto', - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ marginRight: 0, - }, + }), }) type Props = { @@ -75,7 +73,8 @@ export const Layout: React.FC = ({ children, }) => { return ( - + <> + = ({ {sidebarMain} - + ) } diff --git a/src/utilities/helpers.ts b/src/utilities/helpers.ts new file mode 100644 index 0000000..aeb789e --- /dev/null +++ b/src/utilities/helpers.ts @@ -0,0 +1,35 @@ +import config from '../config.json' + +const mangledVarNames: Record = {} +export const configVar = (varName: keyof typeof config) => { + let { [varName]: mangledVarName } = mangledVarNames + if (!mangledVarName) { + mangledVarNames[varName] = mangledVarName = `config-${varName}` + } + + return `var(--${mangledVarName}, ${config[varName]})` +} + +export const loadConfig = (): any => { + return ({ + ':root': Object.entries(mangledVarNames).reduce( + (varNames, [key, cssVarName]) => { + switch (key as keyof typeof config) { + case 'base-width': + if (typeof config[key as keyof typeof config] === 'number') { + return ({ + ...varNames, + [`--${cssVarName}`]: `${config[key as keyof typeof config]}px`, + }) + } + break + } + return ({ + ...varNames, + [`--${cssVarName}`]: config[key as keyof typeof config], + }) + }, + {}, + ), + }) +} diff --git a/src/utilities/mixins.ts b/src/utilities/mixins.ts new file mode 100644 index 0000000..08affbe --- /dev/null +++ b/src/utilities/mixins.ts @@ -0,0 +1,13 @@ +import {CSSObject} from 'styled-components' +import config from '../config.json' + +export const minWidthFactor = (factor: number) => (styles: CSSObject) => ({ + [`@media (min-width: ${config['base-width'] * factor}px)`]: styles, +}) + +export const applyBackgroundColor = () => ({ + backgroundColor: 'var(--color-bg, white)', + '@media (prefers-color-scheme: dark)': { + backgroundColor: 'var(--color-bg, black)', + }, +}) diff --git a/src/widgets/TopBar/index.tsx b/src/widgets/TopBar/index.tsx index 8386bea..2b162b2 100644 --- a/src/widgets/TopBar/index.tsx +++ b/src/widgets/TopBar/index.tsx @@ -1,5 +1,7 @@ import * as React from 'react' import styled from 'styled-components' +import {applyBackgroundColor, minWidthFactor} from '../../utilities/mixins' +import {configVar} from '../../utilities/helpers' const Base = styled('div')({ position: 'fixed', @@ -7,29 +9,26 @@ const Base = styled('div')({ left: 0, width: '100%', height: 'var(--height-topbar, 4rem)', - backgroundColor: 'var(--color-bg, white)', zIndex: 2, - '@media (prefers-color-scheme: dark)': { - backgroundColor: 'var(--color-bg, black)', - }, + ...applyBackgroundColor(), '~ *': { paddingTop: 'var(--height-topbar, 4rem)', }, '~ main ~ *': { paddingTop: 0, }, - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ '~ main ~ *': { paddingTop: 'var(--height-topbar, 4rem)', }, - }, + }), }) const Container = styled('div')({ padding: '0 1rem', boxSizing: 'border-box', margin: '0 auto', - maxWidth: 'calc(var(--width-base, 360px) * 2)', + maxWidth: `calc(${configVar('base-width')} * 2)`, width: '100%', height: '100%', display: 'flex', @@ -37,9 +36,9 @@ const Container = styled('div')({ }) const WideContainer = styled(Container)({ - '@media (min-width: 1080px)': { - maxWidth: 'calc(var(--width-base, 360px) * 3)', - }, + ...minWidthFactor(3)({ + maxWidth: `calc(${configVar('base-width')} * 3)`, + }), }) const BrandContainer = styled('div')({ @@ -60,9 +59,9 @@ const ActionContainer = styled('div')({ justifyContent: 'flex-end', height: '100%', whiteSpace: 'nowrap', - '@media (min-width: 720px)': { + ...minWidthFactor(2)({ minWidth: '8rem', - }, + }), }) const LinkContainer = styled('div')({ @@ -77,10 +76,10 @@ const LinkContainer = styled('div')({ }) const MenuLinkContainer = styled(LinkContainer)({ - '@media (min-width: 1080px)': { + ...minWidthFactor(3)({ position: 'absolute', left: -999999, - }, + }), }) type Props = { diff --git a/tsconfig.json b/tsconfig.json index 2c85b2d..45896ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,5 +31,6 @@ "forceConsistentCasingInFileNames": true, // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` "noEmit": true, + "resolveJsonModule": true } }