Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add theme package #56669

Draft
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/theme/builders/css-properties-from-theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Internal dependencies
*/

import { defaultTheme } from '../theme.js';
import { themeToCss } from '../utils.js';

printStylesheet( defaultTheme );

function printStylesheet( theme ) {
const css = themeToCss( theme );

const contents = [
`/* Generated by WordPress */`,
'\n\n',
':root {',
'\n',
css,
'\n',
'}',
];

return contents;
}
131 changes: 131 additions & 0 deletions packages/theme/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* External dependencies
*/
import { colord, extend } from 'colord';
import a11yPlugin from 'colord/plugins/a11y';
import namesPlugin from 'colord/plugins/names';

extend( [ namesPlugin, a11yPlugin ] );

const LIGHT_VALUES = [ 100, 98, 95, 92, 89, 87, 83, 73, 55, 48, 39, 13 ];
const DARK_VALUES = [ 1, 11, 16, 19, 22, 18, 29, 38, 43, 73, 80, 93 ];
export const PRIMARY_DEFAULT = '#3858e9';

// map showing which lightness in scale each use case should use
const COLOR_MAP = {
bg: {
default: 2,
hover: 3,
active: 4,
input: {
default: 0,
disabled: 0,
},
muted: 1,
strong: {
default: 8,
hover: 9,
},
},
text: {
default: 10,
hover: 11,
strong: 11,
inverse: {
default: 1,
strong: 0,
},
muted: 9,
},
border: {
default: 5,
disabled: 4,
input: 6,
strong: {
default: 6,
hover: 7,
},
muted: 4,
hover: 6,
},
};

// generates a color palette based on a primary color
export const generateColors = ( {
color = PRIMARY_DEFAULT,
fun = 0,
isDark = false,
} ) => {
const neutral = generateNeutralColors( { color, fun, isDark } );
const primary = generatePrimaryColors( {
color,
bg: neutral.bg.default,
isDark,
} );

return {
primary,
neutral,
};
};

const generateNeutralColors = ( {
color = PRIMARY_DEFAULT,
fun = 0,
isDark = false,
} ) => {
const base = colord( color ).toHsl();
const lightValues = isDark ? DARK_VALUES : LIGHT_VALUES;
const colors = lightValues.map( ( value ) =>
colord( { ...base, s: fun, l: value } ).toHex()
);
return mapColors( colors, COLOR_MAP );
};

const generatePrimaryColors = ( {
color = PRIMARY_DEFAULT,
bg,
isDark = false,
} ) => {
const base = colord( color ).toHsl();
const lightValues = isDark ? DARK_VALUES : LIGHT_VALUES;

// if the color given has enough contrast agains the background, use that as the solid background colour and adjust the surrounding scale to proportionally move with it
const length = lightValues.length;
// Calculate the difference between the new value and the old value
const diff = base.l - lightValues[ 8 ];
// Calculate the weight for adjusting values. Closer to base colour should adjust more.
const weight = ( index ) => 1 - Math.abs( 8 - index ) / ( length - 1 );
// Adjust all values in the array based on their weight
let adjustedArray = [ ...lightValues ];
if ( colord( bg ).isReadable( base ) ) {
adjustedArray = lightValues.map( ( value, index ) => {
const adjustment = diff * weight( index );
return index === 8 ? base.l : value + adjustment;
} );
}

// convert colours to hex and set min and max lightness values
const colors = adjustedArray.map( ( value ) =>
colord( {
...base,
l: Math.min( Math.max( parseInt( value ), 0 ), 100 ),
} ).toHex()
);

return mapColors( colors, COLOR_MAP );
};

// maps a color map to a color palette
const mapColors = ( mapFromArray, mapToObject ) => {
const map = {};
Object.keys( mapToObject ).forEach( ( alias ) => {
const color = mapToObject[ alias ];
if ( typeof color === 'object' ) {
map[ alias ] = mapColors( mapFromArray, color );
} else {
map[ alias ] = mapFromArray[ parseInt( color ) ];
}
} );
return map;
};
2 changes: 2 additions & 0 deletions packages/theme/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ThemeProvider } from './provider';
export { defaultTheme } from './theme';
Comment on lines +1 to +2
Copy link
Contributor

@ciampo ciampo Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These exports should probably be private, so that we can safely iterate over the APIs of the package while we're actively developing it

Also, why is the defaultTheme an export? Wouldn't consumers of the theme just need the CSS variables ?

34 changes: 34 additions & 0 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@wordpress/theme",
"version": "1.0.0",
"description": "A collection of tokens that make up a WordPress theme.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"theme",
"variables",
"styles"
],
"homepage": "https://github.com/WordPress/gutenberg",
"repository": {
"type": "git",
"url": "git+https://github.com/WordPress/gutenberg.git",
"directory": "packages/theme"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"files": [
"index.js",
"style.scss"
],
"main": "index.js",
"style": "style.scss",
"dependencies": {
"@wordpress/element": "file:../element",
"colord": "^2.7.0"
},
"publishConfig": {
"access": "public"
}
}
60 changes: 60 additions & 0 deletions packages/theme/provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* WordPress dependencies
*/
import { createElement } from '@wordpress/element';

/**
* Internal dependencies
*/
import { generateColors } from './color';
import { themeToCss } from './utils';

// lightweight way to add styles to a class name
const toHash = ( str ) => {
let i = 0,
out = 11;
while ( i < str.length ) out = ( 101 * out + str.charCodeAt( i++ ) ) >>> 0; //eslint-disable-line no-bitwise
return 'wp-' + out;
};

const addStyle = ( target, className, cssText ) => {
const style = document.createElement( 'style' );
style.id = className;
style.append( cssText );
target.append( style );
};

const merge = ( compiled, target ) => {
const name = toHash( compiled );
if ( ! document.getElementById( name ) ) {
addStyle( target, name, `.${ name } { ${ compiled }}` );
}
return name;
};

// theme provider component that generates a theme and adds appropriate tokens to the head
export const ThemeProvider = ( {
as = 'div',
color,
fun,
isDark,
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid a lot of props in the ThemeProvider component, should there be a color prop of type

{
  primary: string;
  fun: number;
  isDark: boolean;
}

...props
} ) => {
const { className, children, ...rest } = props;
const styles = themeToCss( {
color: generateColors( {
color,
fun,
isDark,
} ),
} );
const name = merge( styles, document.head );
return createElement(
as,
{
className: [ name, className ].join( ' ' ),
...rest,
},
children
);
};
25 changes: 25 additions & 0 deletions packages/theme/style.scss
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's a good idea to commit a file that could be generated at build time?

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:root {
--wp-theme-color-neutral-bg-surface: var(--wp-theme-color-neutral-1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the naming convention, what would be other values that could be used instead of "neutral"? Would "primary" be such an alternative? If so, why aren't there any --wp-theme-color-primary variables in this file?

--wp-theme-color-neutral-bg-input: var(--wp-theme-color-neutral-1);
--wp-theme-color-neutral-text-inverse-strong: var(--wp-theme-color-neutral-1);
--wp-theme-color-neutral-bg-input-disabled: var(--wp-theme-color-neutral-1);
--wp-theme-color-neutral-bg-muted: var(--wp-theme-color-neutral-2);
--wp-theme-color-neutral-text-inverse: var(--wp-theme-color-neutral-2);
--wp-theme-color-neutral-bg: var(--wp-theme-color-neutral-3);
--wp-theme-color-neutral-bg-hover: var(--wp-theme-color-neutral-4);
--wp-theme-color-neutral-bg-active: var(--wp-theme-color-neutral-5);
--wp-theme-color-neutral-border: var(--wp-theme-color-neutral-6);
--wp-theme-color-neutral-border-disabled: var(--wp-theme-color-neutral-6);
--wp-theme-color-neutral-border-input: var(--wp-theme-color-neutral-7);
--wp-theme-color-neutral-border-strong: var(--wp-theme-color-neutral-7);
--wp-theme-color-neutral-border-hover: var(--wp-theme-color-neutral-7);
--wp-theme-color-neutral-border-strong-hover: var(--wp-theme-color-neutral-8);
--wp-theme-color-neutral-bg-strong: var(--wp-theme-color-neutral-9);
--wp-theme-color-neutral-bg-strong-hover: var(--wp-theme-color-neutral-10);
--wp-theme-color-neutral-text-muted: var(--wp-theme-color-neutral-10);
--wp-theme-color-neutral-text: var(--wp-theme-color-neutral-11);
--wp-theme-color-neutral-text-hover: var(--wp-theme-color-neutral-12);
--wp-theme-color-neutral-text-strong: var(--wp-theme-color-neutral-12);
--wp-theme-color-neutral-bg-inverse: var(--wp-theme-color-neutral-12);
}

16 changes: 16 additions & 0 deletions packages/theme/theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Internal dependencies
*/
import { generateColors } from './color';

// // theme object
export const defaultTheme = {
// shadows: {...},
// spacing: { ... },
// borderRadius: { ... },
// fonts: { ... },
// fontSizes: { ... },
// fontWeights: { ... },
// lineHeights: { ... },
colors: generateColors( {} ),
};
20 changes: 20 additions & 0 deletions packages/theme/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// flattens the theme object to a single level
function flattenTheme( obj, parent, res = {} ) {
for ( const key in obj ) {
const propName = parent ? parent + '-' + key : key;
if ( typeof obj[ key ] === 'object' ) {
flattenTheme( obj[ key ], propName, res );
} else {
res[ propName.replace( '-default', '' ) ] = obj[ key ];
}
}
return res;
}

// converts a theme object to a CSS string containing CSS variables
export const themeToCss = ( theme ) => {
const flattenedTheme = flattenTheme( theme );
return Object.entries( flattenedTheme )
.map( ( [ key, value ] ) => `--wp-theme-${ key }: ${ value };` )
.join( '\n' );
};
Loading