diff --git a/src/global-style.tsx b/src/global-style.tsx index cf0170c..121f6ca 100644 --- a/src/global-style.tsx +++ b/src/global-style.tsx @@ -1,16 +1,20 @@ import { css } from "@emotion/react"; -import "normalize.css"; +import { mq } from "./style-predefines"; -export const globalStyle = css` - @import-normalize; - - :root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 62.5%; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - } -`; +export const globalStyle = (theme) => + css({ + [":root"]: { + colorScheme: "light dark", + fontFamily: "Inter, Avenir, Helvetica, Arial, sans-serif", + fontSize: 0.625, + fontSynthesis: "none", + textRendering: "optimizeLegibility", + WebkitFontSmoothing: "antialiased", + MozOsxFontSmoothing: "grayscale", + WebkitTextSizeAdjust: "100%", + backgroundColor: theme.backgroundColor.light, + [mq.dark]: { + backgroundColor: theme.backgroundColor.dark, + }, + }, + }); diff --git a/src/style-predefines.tsx b/src/style-predefines.tsx new file mode 100644 index 0000000..8cd43b9 --- /dev/null +++ b/src/style-predefines.tsx @@ -0,0 +1,104 @@ +import { css, Elevation, FontSizeUnit, MeasureUnit, Theme } from "@emotion/react"; +import chroma from "chroma-js"; +import type { Property } from "csstype"; + +export function generatePalette(lightest: string, darkest: string): ColorPalette { + return chroma.scale([lightest, darkest]).mode("lch").colors(10, "hex"); +} + +export const mq = { + dark: `@media (prefers-color-scheme: dark)`, + [576]: `@media (min-width: 576px)`, + [768]: `@media (min-width: 768px)`, + [992]: `@media (min-width: 992px)`, + [1200]: `@media (min-width: 1200px)`, +}; + +function transformShadowDefine( + shadow: Elevation, + shadowColor?: chroma.Color = chroma.hsl(0, 0, 0) +): string[] { + return [ + `${shadow.offset.width}px`, + `${shadow.offset.height}px`, + `${shadow.blur}px`, + shadowColor.alpha(shadow.opacity).hex(), + ]; +} + +export function elevation(theme: Theme, level: MeasureUnit) { + return css({ boxShadow: transformShadowDefine(theme.elevations[level]) }); +} + +export function flex( + theme: Theme, + direction: Property.FlexDirection = "row", + justify: Property.JustifyContent = "flex-start", + align: Property.AlignItems = "start", + wrap: Property.FlexWrap = "nowrap", + gap: MeasureUnit = "none" +) { + return css({ + display: "flex", + flexDirection: direction, + justifyContent: justify, + alignItems: align, + flexWrap: wrap, + gap: `${theme.spacings[gap]}px`, + }); +} + +export function paddingHorizontal(theme: Theme, padding: MeasureUnit | number = "none") { + switch (typeof padding) { + case "number": + return css({ paddingLeft: padding, paddingRight: padding }); + case "string": + return css({ + paddingLeft: theme.spacings[padding], + paddingRight: theme.spacings[padding], + }); + } +} + +export function paddingVertical(theme: Theme, padding: MeasureUnit | number = "none") { + switch (typeof padding) { + case "number": + return css({ paddingTop: padding, paddingBottom: padding }); + case "string": + return css({ + paddingTop: theme.spacings[padding], + paddingBottom: theme.spacings[padding], + }); + } +} + +export function marginHorizontal(theme: Theme, margin: MeasureUnit | number = "none") { + switch (typeof margin) { + case "number": + return css({ marginLeft: margin, marginRight: margin }); + case "string": + return css({ + marginLeft: theme.spacings[margin], + marginRight: theme.spacings[margin], + }); + } +} + +export function marginVertical(theme: Theme, margin: MeasureUnit | number = "none") { + switch (typeof margin) { + case "number": + return css({ marginTop: margin, marginBottom: margin }); + case "string": + return css({ + marginTop: theme.spacings[margin], + marginBottom: theme.spacings[margin], + }); + } +} + +export function Typography(theme: Theme, fontSize: FontSizeUnit, lineHeightRatio: number = 1.3) { + return css({ + fontSize: theme.fontSizes[fontSize], + lineHeight: theme.fontSizes[fontSize] * lineHeightRatio, + }); +} diff --git a/src/theme.tsx b/src/theme.tsx new file mode 100644 index 0000000..6e5478a --- /dev/null +++ b/src/theme.tsx @@ -0,0 +1,142 @@ +import type { Theme } from "@emotion/react"; +import { generatePalette } from "./style-predefines"; + +const colors: Theme["colors"] = { + dark: generatePalette("#c9c9c9", "#141414"), + gray: generatePalette("#f8f9fa", "#212529"), + red: generatePalette("#fff5f5", "#c92a2a"), + pink: generatePalette("#fff0f6", "#a61e4d"), + grape: generatePalette("#f8f0fc", "#862e9c"), + violet: generatePalette("#f3f0ff", "#5f3dc4"), + indigo: generatePalette("#edf2ff", "#364fc7"), + bllue: generatePalette("#e7f5ff", "#1864ab"), + cyan: generatePalette("#e3fafc", "#0b7285"), + teal: generatePalette("#e6fcf5", "#087f5b"), + green: generatePalette("#ebfbee", "#2b8a3e"), + lime: generatePalette("#f4fce3", "#5c940d"), + yellow: generatePalette("#fff9db", "#e67700"), + orange: generatePalette("#fff4e6", "#d9480f"), +}; + +export const theme: Partial = { + colors, + white: "#ffffff", + black: "#000000", + foregroundColor: { + light: chroma.hsl(0, 0, 0.06).hex(), + dark: chroma.hsl(0, 0, 0.88).hex(), + }, + backgroundColor: { + light: chroma.hsl(0, 0, 0.96).hex(), + dark: chroma.hsl(0, 0, 0.18).hex(), + }, + successColor: { + light: colors.green[5], + dark: colors.green[7], + }, + warnColor: { + light: colors.yellow[5], + dark: colors.yellow[7], + }, + dangerColor: { + light: colors.red[5], + dark: colors.red[7], + }, + infoColor: { + light: colors.blue[5], + dark: colors.blue[7], + }, + spacings: { + none: 0, + xxs: 2, + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 40, + }, + radius: { + none: 0, + xxs: 1, + xs: 2, + sm: 4, + md: 8, + lg: 16, + xl: 24, + xxl: 32, + }, + fontSizes: { + fp: 56, + fps: 48, + f1: 34.7, + f1s: 32, + f2: 29.3, + f2s: 24, + f3: 21.3, + f3s: 20, + f4: 18.7, + f4s: 16, + f5: 14, + f5s: 12, + f6: 10, + f6s: 8.7, + f7: 7.3, + f7s: 6.7, + }, + paragraphFontSize: 14, + titleFontSize: { + h1: 56, + h2: 34.7, + h3: 29.3, + h4: 21.3, + h5: 18.7, + h6: 14, + }, + elevationColor: { + light: chroma.hsl(0, 0, 0).alpha(0.2).hex(), + dark: chroma.hsl(0, 0, 0).alpha(0.2).hex(), + }, + elevations: { + none: { + offset: { width: 0, height: 0 }, + blur: 0, + opacity: 0, + }, + xxs: { + offset: { width: 1, height: 1 }, + blur: 2, + opacity: 0.2, + }, + xs: { + offset: { width: 2, height: 2 }, + blur: 4, + opacity: 0.2, + }, + sm: { + offset: { width: 3, height: 3 }, + blur: 6, + opacity: 0.2, + }, + md: { + offset: { width: 4, height: 4 }, + blur: 8, + opacity: 0.2, + }, + lg: { + offset: { width: 5, height: 5 }, + blur: 10, + opacity: 0.2, + }, + xl: { + offset: { width: 6, height: 6 }, + blur: 12, + opacity: 0.2, + }, + xxl: { + offset: { width: 7, height: 7 }, + blur: 14, + opacity: 0.2, + }, + }, +}; diff --git a/src/typings/theme.d.ts b/src/typings/theme.d.ts new file mode 100644 index 0000000..81fcbc7 --- /dev/null +++ b/src/typings/theme.d.ts @@ -0,0 +1,69 @@ +import "@emotion/react"; + +declare module "@emotion/react" { + export type ColorName = + | "dark" + | "gray" + | "red" + | "pink" + | "grape" + | "violet" + | "indigo" + | "blue" + | "cyan" + | "teal" + | "green" + | "lime" + | "yellow" + | "orange"; + export type ColorScheme = "light" | "dark"; + export type MeasureUnit = "none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl"; + export type FontSizeUnit = + | "fp" + | "fps" + | "f1" + | "f1s" + | "f2" + | "f2s" + | "f3" + | "f3s" + | "f4" + | "f4s" + | "f5" + | "f5s" + | "f6" + | "f6s" + | "f7" + | "f7s"; + export type TitleLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + export type ColorPalette = Array; + export type Elevation = { + offset: { + width: number; + height: number; + }; + blur: number; + opacity: number; + }; + + export interface Theme { + colors: Record; + black: string; + white: string; + primaryColor: Record; + secondaryColor: Record; + backgroundColor: Record; + foregroundColor: Record; + successColor: Record; + warnColor: Record; + dangerColor: Record; + infoColor: Record; + spacings: Record; + radius: Record; + fontSizes: Record; + paragraphFontSize: number; + titleFontSize: Record; + elevations: Record; + elevationColor: Record; + } +}