From 5bd131db3fd8a91119418cdcbfa6ebcb82006ef9 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 1 May 2021 22:41:00 +0800 Subject: [PATCH] Make styles dynamic The CSS variables have been documented to help with guiding the user for configuration. The components have been adjusted to define variables internally. They will use internal defaults but will inherit their ascendant's CSS variables. --- README.md | 21 +++- package.json | 2 +- src/config.json | 3 + src/layouts/Basic/index.tsx | 17 +-- src/layouts/LeftSidebar/index.tsx | 57 +++++---- src/layouts/LeftSidebarWithMenu/index.tsx | 145 ++++++++++------------ src/layouts/RightSidebarStatic/index.tsx | 49 ++++---- src/utilities/helpers.ts | 35 ++++++ src/utilities/mixins.ts | 13 ++ src/widgets/TopBar/index.tsx | 27 ++-- tsconfig.json | 1 + 11 files changed, 214 insertions(+), 156 deletions(-) create mode 100644 src/config.json create mode 100644 src/utilities/helpers.ts create mode 100644 src/utilities/mixins.ts 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 } }