From 1c910248bf382b0fbd77d17eeada0dd240d9f1c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 7 Mar 2022 16:54:45 -0600 Subject: [PATCH 01/10] Set Tailwind colors with CSS variables --- app/styles/themes.scss | 46 +++++++++++++++++++- tailwind.config.js | 97 +++++++++++++++++------------------------- 2 files changed, 85 insertions(+), 58 deletions(-) diff --git a/app/styles/themes.scss b/app/styles/themes.scss index d23808161..112c2abc1 100644 --- a/app/styles/themes.scss +++ b/app/styles/themes.scss @@ -26,6 +26,50 @@ Examples: body, .site-preview { + // Tailwind color variables + --color-gray-50: 249 250 251; + --color-gray-100: 243 244 246; + --color-gray-200: 229 231 235; + --color-gray-300: 209 213 219; + --color-gray-400: 156 163 175; + --color-gray-500: 107 114 128; + --color-gray-600: 75 85 99; + --color-gray-700: 55 65 81; + --color-gray-800: 31 41 55; + --color-gray-900: 17 24 39; + --color-primary-50: 238 242 255; + --color-primary-100: 224 231 255; + --color-primary-200: 199 210 254; + --color-primary-300: 165 180 252; + --color-primary-400: 129 140 248; + --color-primary-500: 99 102 241; + --color-primary-600: 84 72 238; + --color-primary-700: 67 56 202; + --color-primary-800: 55 48 163; + --color-primary-900: 49 46 129; + --color-success-50: 240 253 244; + --color-success-100: 220 252 231; + --color-success-200: 187 247 208; + --color-success-300: 134 239 172; + --color-success-400: 74 222 128; + --color-success-500: 34 197 94; + --color-success-600: 22 163 74; + --color-success-700: 21 128 61; + --color-success-800: 22 101 52; + --color-success-900: 20 83 45; + --color-danger-50: 254 242 242; + --color-danger-100: 254 226 226; + --color-danger-200: 254 202 202; + --color-danger-300: 252 165 165; + --color-danger-400: 248 113 113; + --color-danger-500: 239 68 68; + --color-danger-600: 220 38 38; + --color-danger-700: 185 28 28; + --color-danger-800: 153 27 27; + --color-danger-900: 127 29 29; + --color-accent-300: 255 95 135; + --color-accent-500: 255 71 117; + // Primary variables --brand-color: hsl(var(--brand-color_hsl)); --accent-color: hsl(var(--accent-color_hsl)); @@ -82,7 +126,7 @@ body, --input-border-color: #d1d5db; // Typography - --font-sans: 'Inter', Arial, sans-serif; + --font-sans: 'Inter', arial, sans-serif; --font-weight-heading: 700; --font-weight-body: 400; } diff --git a/tailwind.config.js b/tailwind.config.js index 9b88abf0d..e697ea757 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,3 +1,38 @@ +// https://tailwindcss.com/docs/customizing-colors#using-css-variables +function withOpacityValue(variable) { + return ({ opacityValue }) => { + if (opacityValue === undefined) { + return `rgb(var(${variable}))`; + } + return `rgb(var(${variable}) / ${opacityValue})`; + }; +} + +// Parse list of shades into Tailwind function with CSS variables +const parseShades = (color, shades) => { + return shades.reduce((obj, shade) => { + obj[shade] = withOpacityValue(`--color-${color}-${shade}`); + return obj; + }, {}); +}; + +// Parse color matrix into Tailwind colors object +const parseColors = colors => { + return Object.keys(colors).reduce((obj, color) => { + obj[color] = parseShades(color, colors[color]); + return obj; + }, {}); +}; + +// Define color matrix (of available colors) +const colors = { + gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + accent: [300, 500], +}; + module.exports = { content: ['./app/**/*.{html,js,ts,tsx}'], darkMode: 'class', @@ -31,63 +66,11 @@ module.exports = { 'Noto Color Emoji', ], }, - colors: { - gray: { - 50: '#f9fafb', - 100: '#f3f4f6', - 200: '#e5e7eb', - 300: '#d1d5db', - 400: '#9ca3af', - 500: '#6b7280', - 600: '#4b5563', - 700: '#374151', - 800: '#1f2937', - 900: '#111827', - }, - primary: { - 50: '#eef2ff', - 100: '#e0e7ff', - 200: '#c7d2fe', - 300: '#a5b4fc', - 400: '#818cf8', - 500: '#6366f1', - 600: '#5448ee', - 700: '#4338ca', - 800: '#3730a3', - 900: '#312e81', - }, - success: { - 50: '#f0fdf4', - 100: '#dcfce7', - 200: '#bbf7d0', - 300: '#86efac', - 400: '#4ade80', - 500: '#22c55e', - 600: '#16a34a', - 700: '#15803d', - 800: '#166534', - 900: '#14532d', - }, - danger: { - 50: '#fef2f2', - 100: '#fee2e2', - 200: '#fecaca', - 300: '#fca5a5', - 400: '#f87171', - 500: '#ef4444', - 600: '#dc2626', - 700: '#b91c1c', - 800: '#991b1b', - 900: '#7f1d1d', - }, - accent: { - 300: '#ff5f87', - 500: '#ff4775', - }, - 'gradient-purple': '#b8a3f9', - 'gradient-blue': '#9bd5ff', - 'sea-blue': '#2feecc', - }, + colors: Object.assign(parseColors(colors), { + 'gradient-purple': withOpacityValue('--color-gradient-purple'), + 'gradient-blue': withOpacityValue('--color-gradient-blue'), + 'sea-blue': withOpacityValue('--color-sea-blue'), + }), }, }, plugins: [ From 0938612678ba83c73764dc336f282d6debe45728 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 7 Mar 2022 17:19:54 -0600 Subject: [PATCH 02/10] Use colors from soapbox.json at runtime --- app/soapbox/actions/soapbox.js | 1 + app/soapbox/containers/soapbox.js | 6 ++++- app/soapbox/utils/theme.js | 26 ++++++++++++++++++ app/styles/themes.scss | 44 ------------------------------- 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 91e1a26f2..ac8749221 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -41,6 +41,7 @@ export const makeDefaultConfig = features => { banner: '', brandColor: '', // Empty accentColor: '', + colors: ImmutableMap(), customCss: ImmutableList(), promoPanel: ImmutableMap({ items: ImmutableList(), diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index a86573be7..c702ea12c 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -25,6 +25,7 @@ import { createGlobals } from 'soapbox/globals'; import messages from 'soapbox/locales/messages'; import { makeGetAccount } from 'soapbox/selectors'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; +import { colorsToCss } from 'soapbox/utils/theme'; import { INTRODUCTION_VERSION } from '../actions/onboarding'; import { preload } from '../actions/preload'; @@ -83,6 +84,7 @@ const mapStateToProps = (state) => { dyslexicFont: settings.get('dyslexicFont'), demetricator: settings.get('demetricator'), locale: validLocale(locale) ? locale : 'en', + themeCss: colorsToCss(soapboxConfig.get('colors').toJS()), brandColor: soapboxConfig.get('brandColor'), themeMode: settings.get('themeMode'), singleUserMode, @@ -103,6 +105,7 @@ class SoapboxMount extends React.PureComponent { dyslexicFont: PropTypes.bool, demetricator: PropTypes.bool, locale: PropTypes.string.isRequired, + themeCss: PropTypes.string, themeMode: PropTypes.string, brandColor: PropTypes.string, dispatch: PropTypes.func, @@ -139,7 +142,7 @@ class SoapboxMount extends React.PureComponent { } render() { - const { me, account, instanceLoaded, locale, singleUserMode } = this.props; + const { me, account, instanceLoaded, themeCss, locale, singleUserMode } = this.props; if (me === null) return null; if (me && !account) return null; if (!instanceLoaded) return null; @@ -169,6 +172,7 @@ class SoapboxMount extends React.PureComponent { {/* */} + {themeCss && } diff --git a/app/soapbox/utils/theme.js b/app/soapbox/utils/theme.js index 1b3c85dfe..0f3d63b4f 100644 --- a/app/soapbox/utils/theme.js +++ b/app/soapbox/utils/theme.js @@ -65,6 +65,32 @@ const rgbToHsl = value => { }; }; +const parseShades = (obj, color, shades) => { + if (typeof shades === 'string') { + const { r, g, b } = hexToRgb(shades); + return obj[`--color-${color}`] = `${r} ${g} ${b}`; + } + + return Object.keys(shades).forEach(shade => { + const { r, g, b } = hexToRgb(shades[shade]); + obj[`--color-${color}-${shade}`] = `${r} ${g} ${b}`; + }); +}; + +const parseColors = colors => { + return Object.keys(colors).reduce((obj, color) => { + parseShades(obj, color, colors[color]); + return obj; + }, {}); +}; + +export const colorsToCss = colors => { + const parsed = parseColors(colors); + return Object.keys(parsed).reduce((css, variable) => { + return css + `${variable}:${parsed[variable]};`; + }, ''); +}; + export const brandColorToThemeData = brandColor => { const { h, s, l } = rgbToHsl(hexToRgb(brandColor)); return ImmutableMap({ diff --git a/app/styles/themes.scss b/app/styles/themes.scss index 112c2abc1..97a9b5f95 100644 --- a/app/styles/themes.scss +++ b/app/styles/themes.scss @@ -26,50 +26,6 @@ Examples: body, .site-preview { - // Tailwind color variables - --color-gray-50: 249 250 251; - --color-gray-100: 243 244 246; - --color-gray-200: 229 231 235; - --color-gray-300: 209 213 219; - --color-gray-400: 156 163 175; - --color-gray-500: 107 114 128; - --color-gray-600: 75 85 99; - --color-gray-700: 55 65 81; - --color-gray-800: 31 41 55; - --color-gray-900: 17 24 39; - --color-primary-50: 238 242 255; - --color-primary-100: 224 231 255; - --color-primary-200: 199 210 254; - --color-primary-300: 165 180 252; - --color-primary-400: 129 140 248; - --color-primary-500: 99 102 241; - --color-primary-600: 84 72 238; - --color-primary-700: 67 56 202; - --color-primary-800: 55 48 163; - --color-primary-900: 49 46 129; - --color-success-50: 240 253 244; - --color-success-100: 220 252 231; - --color-success-200: 187 247 208; - --color-success-300: 134 239 172; - --color-success-400: 74 222 128; - --color-success-500: 34 197 94; - --color-success-600: 22 163 74; - --color-success-700: 21 128 61; - --color-success-800: 22 101 52; - --color-success-900: 20 83 45; - --color-danger-50: 254 242 242; - --color-danger-100: 254 226 226; - --color-danger-200: 254 202 202; - --color-danger-300: 252 165 165; - --color-danger-400: 248 113 113; - --color-danger-500: 239 68 68; - --color-danger-600: 220 38 38; - --color-danger-700: 185 28 28; - --color-danger-800: 153 27 27; - --color-danger-900: 127 29 29; - --color-accent-300: 255 95 135; - --color-accent-500: 255 71 117; - // Primary variables --brand-color: hsl(var(--brand-color_hsl)); --accent-color: hsl(var(--accent-color_hsl)); From 18bad4a5ab9e886a2c45c28480cbdea0b637d0d4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Mar 2022 18:57:23 -0500 Subject: [PATCH 03/10] Typescript: utils/theme.ts --- app/soapbox/utils/{theme.js => theme.ts} | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) rename app/soapbox/utils/{theme.js => theme.ts} (73%) diff --git a/app/soapbox/utils/theme.js b/app/soapbox/utils/theme.ts similarity index 73% rename from app/soapbox/utils/theme.js rename to app/soapbox/utils/theme.ts index 0f3d63b4f..e052a41dc 100644 --- a/app/soapbox/utils/theme.js +++ b/app/soapbox/utils/theme.ts @@ -1,15 +1,18 @@ import { Map as ImmutableMap } from 'immutable'; -export const generateThemeCss = (brandColor, accentColor) => { +type RGB = { r: number, g: number, b: number }; +type HSL = { h: number, s: number, l: number }; + +export const generateThemeCss = (brandColor: string, accentColor: string): string => { if (!brandColor) return null; return themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor))); }; // https://stackoverflow.com/a/5624139 -function hexToRgb(hex) { +function hexToRgb(hex: string): RGB { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, (m, r, g, b) => ( + hex = hex.replace(shorthandRegex, (_m, r, g, b) => ( r + r + g + g + b + b )); @@ -28,7 +31,7 @@ function hexToRgb(hex) { // Taken from chromatism.js // https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js -const rgbToHsl = value => { +const rgbToHsl = (value: RGB): HSL => { const r = value.r / 255; const g = value.g / 255; const b = value.b / 255; @@ -65,7 +68,7 @@ const rgbToHsl = value => { }; }; -const parseShades = (obj, color, shades) => { +const parseShades = (obj: Record, color: string, shades: Record) => { if (typeof shades === 'string') { const { r, g, b } = hexToRgb(shades); return obj[`--color-${color}`] = `${r} ${g} ${b}`; @@ -77,21 +80,21 @@ const parseShades = (obj, color, shades) => { }); }; -const parseColors = colors => { +const parseColors = (colors: Record): Record => { return Object.keys(colors).reduce((obj, color) => { parseShades(obj, color, colors[color]); return obj; }, {}); }; -export const colorsToCss = colors => { +export const colorsToCss = (colors: Record): string => { const parsed = parseColors(colors); return Object.keys(parsed).reduce((css, variable) => { return css + `${variable}:${parsed[variable]};`; }, ''); }; -export const brandColorToThemeData = brandColor => { +export const brandColorToThemeData = (brandColor: string): ImmutableMap => { const { h, s, l } = rgbToHsl(hexToRgb(brandColor)); return ImmutableMap({ 'brand-color_h': h, @@ -100,7 +103,7 @@ export const brandColorToThemeData = brandColor => { }); }; -export const accentColorToThemeData = (brandColor, accentColor) => { +export const accentColorToThemeData = (brandColor: string, accentColor: string): ImmutableMap => { if (accentColor) { const { h, s, l } = rgbToHsl(hexToRgb(accentColor)); @@ -119,10 +122,11 @@ export const accentColorToThemeData = (brandColor, accentColor) => { }); }; -export const themeDataToCss = themeData => ( +export const themeDataToCss = (themeData: ImmutableMap): string => ( themeData .entrySeq() .reduce((acc, cur) => acc + `--${cur[0]}:${cur[1]};`, '') ); -export const themeColorsToCSS = (brandColor, accentColor) => themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor))); +export const themeColorsToCSS = (brandColor: string, accentColor: string): string => + themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor))); From a42ea0961aeef51e8427abb19f6b2dec8f27fcef Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 22 Mar 2022 12:37:57 -0500 Subject: [PATCH 04/10] Generate Tailwind colors from brandColor/accentColor --- app/soapbox/containers/soapbox.js | 4 +- app/soapbox/features/ui/components/navbar.tsx | 12 +- app/soapbox/types/colors.ts | 10 ++ app/soapbox/utils/__tests__/colors-test.js | 22 ++++ app/soapbox/utils/colors.ts | 123 ++++++++++++++++++ app/soapbox/utils/theme.ts | 104 ++++++--------- 6 files changed, 203 insertions(+), 72 deletions(-) create mode 100644 app/soapbox/types/colors.ts create mode 100644 app/soapbox/utils/__tests__/colors-test.js create mode 100644 app/soapbox/utils/colors.ts diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index c702ea12c..aad96badb 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -25,7 +25,7 @@ import { createGlobals } from 'soapbox/globals'; import messages from 'soapbox/locales/messages'; import { makeGetAccount } from 'soapbox/selectors'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; -import { colorsToCss } from 'soapbox/utils/theme'; +import { themeColorsToCSS } from 'soapbox/utils/theme'; import { INTRODUCTION_VERSION } from '../actions/onboarding'; import { preload } from '../actions/preload'; @@ -84,7 +84,7 @@ const mapStateToProps = (state) => { dyslexicFont: settings.get('dyslexicFont'), demetricator: settings.get('demetricator'), locale: validLocale(locale) ? locale : 'en', - themeCss: colorsToCss(soapboxConfig.get('colors').toJS()), + themeCss: themeColorsToCSS(soapboxConfig.get('brandColor') || '#0482d8', soapboxConfig.get('accentColor', '')), brandColor: soapboxConfig.get('brandColor'), themeMode: settings.get('themeMode'), singleUserMode, diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index 2f6ffe8df..d9198ecc4 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -33,11 +33,13 @@ const Navbar = () => {