diff --git a/docs/guides/THEMES_IMPL_NOTES.md b/docs/guides/THEMES_IMPL_NOTES.md index 32dacf266ef..e7da47e129a 100644 --- a/docs/guides/THEMES_IMPL_NOTES.md +++ b/docs/guides/THEMES_IMPL_NOTES.md @@ -12,7 +12,7 @@ the `$mdTheming` service and tacked into the document head. * Instead of using hard-coded color or a SCSS variable, the colors are defined with a mini-DSL (described deblow). * The build process takes all of those `-theme.scss` files and globs them up into one enourmous -string. +string. * The build process wraps that string with code to set it an angular module constant: ``` angular.module('material.core').constant('$MD_THEME_CSS', 'HUGE_THEME_STRING'); ``` * That code gets dumped at the end of `angular-material.js` @@ -24,15 +24,21 @@ mini-DSL, applies the colors for the theme, and appends the resulting CSS into t ### The mini-DSL -* Each color is written in the form `'{{palette-hue-opacity}}'`, where opacity is optional. +* Each color is written in the form `'{{palette-hue-contrast-opacity}}'`, where `hue`, `contrast`, +and opacity are optional. * For example, `'{{primary-500}}'` -* Palettes are `primary`, `accent`, `warn`, `background`, `foreground` -* The hues for each type except `foreground` use the Material Design hues. -* The `forground` palette is a number from one to four: - * `foreground-1`: text - * `foreground-2`: secondary text, icons - * `foreground-3`: disabled text, hint text - * `foreground-4`: dividers -* There is also a special hue called `contrast` that will give a contrast color (for text). -For example, `accent-contrast` will be a contrast color for the accent color, for use as a text -color on an accent-colored background. +* Palettes are `primary`, `accent`, `warn`, `background` +* The hues for each type use the Material Design hues. When not specified, each palette defaults +`hue` to `500` with the exception of `background` +* The `opacity` value can be a decimal between 0 and 1 or one of the following values based on the +hue's contrast type (dark, light, or strongLight): + * `icon`: icon (0.54 / 0.87 / 1.0) + * `secondary`: secondary text (0.54 / 0.87) + * `disabled`: disabled text or icon (0.38 / 0.54) + * `hint`: hint text (0.38 / 0.50) + * `divider`: divider (0.12) +* `contrast` will give a contrast color (for text) and can be mixed with `opacity`. +For example, `accent-contrast` will be a contrast color for the accent color, for use as a text +color on an accent-colored background. Adding an `opacity` value as in `accent-contrast-icon` will +apply the Material Design icon opacity. Using a decimal opacity value as in `accent-contrast-0.25` +will apply the contrast color for the accent color at 25% opacity. \ No newline at end of file diff --git a/src/core/services/theming/theming.js b/src/core/services/theming/theming.js index cd562b996be..33689c0e905 100644 --- a/src/core/services/theming/theming.js +++ b/src/core/services/theming/theming.js @@ -168,13 +168,9 @@ function detectDisabledThemes($mdThemingProvider) { * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules - * - * Foreground expansion: Applies rgba to black/white foreground text - * - * {{foreground-1}} - used for primary text - * {{foreground-2}} - used for secondary text/divider - * {{foreground-3}} - used for disabled text - * {{foreground-4}} - used for dividers + * {{primary-contrast-divider}} - Apply divider opacity to contrast color + * {{background-default-contrast}} - Apply primary text color for contrasting with default background + * {{background-50-contrast-icon}} - Apply contrast color for icon on background's shade 50 hue * */ @@ -184,21 +180,14 @@ var GENERATED = { }; // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; -// Text Colors on light and dark backgrounds +// Text colors are automatically generated based on background color when not specified +// Custom palettes can provide override colors // @see https://www.google.com/design/spec/style/color.html#color-text-background-colors var DARK_FOREGROUND = { name: 'dark', - '1': 'rgba(0,0,0,0.87)', - '2': 'rgba(0,0,0,0.54)', - '3': 'rgba(0,0,0,0.38)', - '4': 'rgba(0,0,0,0.12)' }; var LIGHT_FOREGROUND = { name: 'light', - '1': 'rgba(255,255,255,1.0)', - '2': 'rgba(255,255,255,0.7)', - '3': 'rgba(255,255,255,0.5)', - '4': 'rgba(255,255,255,0.12)' }; var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; @@ -235,6 +224,34 @@ var DARK_DEFAULT_HUES = { 'hue-3': 'A200' } }; + +// use inactive icon opacity from https://material.google.com/style/color.html#color-text-background-colors +// not inactive icon opacity from https://material.google.com/style/icons.html#icons-system-icons + +var DARK_CONTRAST_OPACITY = { + 'icon': 0.54, + 'secondary': 0.54, + 'disabled': 0.38, + 'hint': 0.38, + 'divider': 0.12, +}; + +var LIGHT_CONTRAST_OPACITY = { + 'icon': 0.87, + 'secondary': 0.7, + 'disabled': 0.5, + 'hint': 0.5, + 'divider': 0.12 +}; + +var STRONG_LIGHT_CONTRAST_OPACITY = { + 'icon': 1.0, + 'secondary': 0.7, + 'disabled': 0.5, + 'hint': 0.5, + 'divider': 0.12 +}; + THEME_COLOR_TYPES.forEach(function(colorType) { // Color types with unspecified default hues will use these default hue values var defaultDefaultHues = { @@ -861,20 +878,44 @@ function parseRules(theme, colorType, rules) { var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); // Matches '{{ primary-color }}', etc - var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); - var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; + var hueRegex = new RegExp('(?:\'|")?{{\\s*(' + colorType + ')-?(color|default)?-?(contrast)?-?((?:\\d\\.?\\d*)|(?:[a-zA-Z]+))?\\s*}}(\"|\')?','g'); + var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(contrast)?-?((?:\d\.?\d*)|(?:[a-zA-Z]+))?\s*\}\}'?"?/g; var palette = PALETTES[color.name]; + var defaultBgHue = theme.colors['background'].hues['default']; + var defaultBgContrastType = PALETTES[theme.colors['background'].name][defaultBgHue].contrastType; // find and replace simple variables where we use a specific hue, not an entire palette // eg. "{{primary-100}}" //\(' + THEME_COLOR_TYPES.join('\|') + '\)' - rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { + rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, contrast, opacity) { + var regexColorType = colorType; if (colorType === 'foreground') { if (hue == 'shadow') { return theme.foregroundShadow; - } else { - return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; + } else if (theme.foregroundPalette[hue]) { + // Use user defined palette number (ie: foreground-2) + return rgba( colorToRgbaArray( theme.foregroundPalette[hue] ) ); + } else if (theme.foregroundPalette['1']){ + return rgba( colorToRgbaArray( theme.foregroundPalette['1'] ) ); + } + // Default to background-default-contrast-{opacity} + colorType = 'background'; + contrast = 'contrast'; + if (!opacity && hue) { + // Convert references to legacy hues to opacities (ie: foreground-4 to *-divider) + switch(hue) { + // hue-1 uses default opacity + case '2': + opacity = 'secondary'; + break; + case '3': + opacity = 'disabled'; + break; + case '4': + opacity = 'divider'; + } } + hue = 'default'; } // `default` is also accepted as a hue-value, because the background palettes are @@ -883,13 +924,51 @@ function parseRules(theme, colorType, rules) { hue = theme.colors[colorType].hues[hue]; } - return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); + var colorDetails = (PALETTES[ theme.colors[colorType].name ][hue] || ''); + + // If user has specified a foreground color, use those + if (colorType === 'background' && contrast && regexColorType !== 'foreground' && colorDetails.contrastType == defaultBgContrastType) { + // Don't process if colorType was changed + switch (opacity) { + case 'secondary': + case 'icon': + if (theme.foregroundPalette['2']) { + return rgba(colorToRgbaArray(theme.foregroundPalette['2'])); + } + break; + case 'disabled': + case 'hint': + if (theme.foregroundPalette['3']) { + return rgba(colorToRgbaArray(theme.foregroundPalette['3'])); + } + break; + case 'divider': + if (theme.foregroundPalette['4']) { + return rgba(colorToRgbaArray(theme.foregroundPalette['4'])); + } + break; + default: + if (theme.foregroundPalette['1']) { + return rgba(colorToRgbaArray(theme.foregroundPalette['1'])); + } + break; + } + } + + if (contrast && opacity) { + opacity = colorDetails.opacity[opacity] || opacity; + } + + return rgba( colorDetails[contrast ? 'contrast' : 'value'], opacity ); }); // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) angular.forEach(color.hues, function(hueValue, hueName) { var newRule = rules - .replace(hueRegex, function(match, _, colorType, hueType, opacity) { + .replace(hueRegex, function(match, colorType, hueType, contrast, opacity) { + if (contrast && opacity) { + opacity = palette[hueValue].opacity[opacity] || opacity; + } return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); }); if (hueName !== 'default') { @@ -1001,6 +1080,37 @@ function generateAllThemes($injector, $mdTheming) { delete palette.contrastStrongLightColors; delete palette.contrastDarkColors; + function getContrastType(hueName) { + if (defaultContrast === 'light' ? darkColors.indexOf(hueName) !== -1 : lightColors.indexOf(hueName) === -1) { + return 'dark'; + } + if (strongLightColors.indexOf(hueName) !== -1) { + return 'strongLight'; + } + return 'light'; + } + function getContrastColor(contrastType) { + switch(contrastType) { + default: + case 'strongLight': + return STRONG_LIGHT_CONTRAST_COLOR; + case 'light': + return LIGHT_CONTRAST_COLOR; + case 'dark': + return DARK_CONTRAST_COLOR; + } + } + function getOpacityValues(contrastType) { + switch(contrastType) { + default: + case 'strongLight': + return STRONG_LIGHT_CONTRAST_OPACITY; + case 'light': + return LIGHT_CONTRAST_OPACITY; + case 'dark': + return DARK_CONTRAST_OPACITY; + } + } // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } angular.forEach(palette, function(hueValue, hueName) { if (angular.isObject(hueValue)) return; // Already converted @@ -1013,28 +1123,14 @@ function generateAllThemes($injector, $mdTheming) { .replace('%3', hueName)); } + var contrastType = getContrastType(hueName); palette[hueName] = { hex: palette[hueName], value: rgbValue, - contrast: getContrastColor() + contrastType: contrastType, + contrast: getContrastColor(contrastType), + opacity: getOpacityValues(contrastType) }; - function getContrastColor() { - if (defaultContrast === 'light') { - if (darkColors.indexOf(hueName) > -1) { - return DARK_CONTRAST_COLOR; - } else { - return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR - : LIGHT_CONTRAST_COLOR; - } - } else { - if (lightColors.indexOf(hueName) > -1) { - return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR - : LIGHT_CONTRAST_COLOR; - } else { - return DARK_CONTRAST_COLOR; - } - } - } }); } } diff --git a/src/core/services/theming/theming.spec.js b/src/core/services/theming/theming.spec.js index 047da92a631..26031010015 100644 --- a/src/core/services/theming/theming.spec.js +++ b/src/core/services/theming/theming.spec.js @@ -249,11 +249,29 @@ describe('$mdThemingProvider', function() { .toEqual('color: rgba(0,0,0,0.12);'); expect(parse('.md-THEME_NAME-theme { color: "{{foreground-shadow}}"; }')[0].content) .toEqual('color: ;'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-icon}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.54);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.54);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-disabled}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.38);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-hint}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.38);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-divider}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.12);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-0.05}}"; }')[0].content) + .toEqual('color: rgba(0,0,0,0.05);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.7);'); }); it('for a dark theme', function() { testTheme.dark(); expect(parse('.md-THEME_NAME-theme { color: "{{foreground-1}}"; }')[0].content) - .toEqual('color: rgba(255,255,255,1.0);'); + .toEqual('color: rgba(255,255,255,0.87);'); expect(parse('.md-THEME_NAME-theme { color: "{{foreground-2}}"; }')[0].content) .toEqual('color: rgba(255,255,255,0.7);'); expect(parse('.md-THEME_NAME-theme { color: "{{foreground-3}}"; }')[0].content) @@ -262,6 +280,77 @@ describe('$mdThemingProvider', function() { .toEqual('color: rgba(255,255,255,0.12);'); expect(parse('.md-THEME_NAME-theme { color: "{{foreground-shadow}}"; }')[0].content) .toEqual('color: 1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-icon}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.7);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-disabled}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.5);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-hint}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.5);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-divider}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.12);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-0.05}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.05);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-900-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,255,255);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-900-contrast-icon}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,1);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast-icon}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.7);'); + }); + it('override foreground color', function() { + testTheme.dark(false); + testTheme.foregroundPalette = { + '1': 'ff0000', + '2': '00ff00', + '3': '0000ff', + '4': 'ffff00' + }; + expect(parse('.md-THEME_NAME-theme { color: "{{foreground-1}}"; }')[0].content) + .toEqual('color: rgb(255,0,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{foreground-2}}"; }')[0].content) + .toEqual('color: rgb(0,255,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{foreground-3}}"; }')[0].content) + .toEqual('color: rgb(0,0,255);'); + expect(parse('.md-THEME_NAME-theme { color: "{{foreground-4}}"; }')[0].content) + .toEqual('color: rgb(255,255,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{foreground-shadow}}"; }')[0].content) + .toEqual('color: ;'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,0,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-icon}}"; }')[0].content) + .toEqual('color: rgb(0,255,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgb(0,255,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-disabled}}"; }')[0].content) + .toEqual('color: rgb(0,0,255);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-hint}}"; }')[0].content) + .toEqual('color: rgb(0,0,255);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-default-contrast-divider}}"; }')[0].content) + .toEqual('color: rgb(255,255,0);'); + + // override colors of same contrast type + expect(parse('.md-THEME_NAME-theme { color: "{{background-50-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,0,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-100-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,0,0);'); + expect(parse('.md-THEME_NAME-theme { color: "{{background-200-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,0,0);'); + + // should not override the following + expect(parse('.md-THEME_NAME-theme { color: "{{background-900-contrast}}"; }')[0].content) + .toEqual('color: rgb(255,255,255);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.87);'); + expect(parse('.md-THEME_NAME-theme { color: "{{primary-contrast-secondary}}"; }')[0].content) + .toEqual('color: rgba(255,255,255,0.7);'); }); }); it('parses contrast colors', function() {