diff --git a/src/material/schematics/ng-generate/theme-color/README.md b/src/material/schematics/ng-generate/theme-color/README.md index e7e8faf7406f..354f2a2538c0 100644 --- a/src/material/schematics/ng-generate/theme-color/README.md +++ b/src/material/schematics/ng-generate/theme-color/README.md @@ -3,24 +3,64 @@ ```shell ng generate @angular/material:theme-color ``` - +## Background This schematic allows users to create new Material 3 theme palettes based on custom colors by using [Material Color Utilities](https://github.com/material-foundation/material-color-utilities). +This is an alternative to using the available [predefined theme palettes](https://material.angular.io/guide/theming#prebuilt-color-palettes). The generated [color palettes](https://m3.material.io/styles/color/roles) are -optimized to have enough contrast to be more accessible. See [Science of Color Design](https://material.io/blog/science-of-color-design) for more information about Material's color design. +optimized to have enough contrast to be more accessible. See [Science of Color Design](https://material.io/blog/science-of-color-design) +for more information about Material's color design. For more customization, custom colors can be also be provided for the secondary, tertiary, and neutral palette colors. It is recommended to choose colors that -are contrastful, Material has more detailed guidance for [accessible design](https://m3.material.io/foundations/accessible-design/patterns). +are contrastful. Material has more detailed guidance for [accessible design](https://m3.material.io/foundations/accessible-design/patterns). + +## Options + +### Required +* `primaryColor` - Color to use for app's primary color palette (Note: the other +palettes described in the Material 3 spec will be automatically chosen based on +your primary palette unless specified, to ensure a harmonious color combination). + +### Optional +* `secondaryColor` - Color to use for app's secondary color palette. Defaults to +secondary color generated from Material based on the primary. +* `tertiaryColor` - Color to use for app's tertiary color palette. Defaults to +tertiary color generated from Material based on the primary. +* `neutralColor` - Color to use for app's neutral color palette. Defaults to +neutral color generated from Material based on the primary. +* `includeHighContrast` - Whether to define high contrast values for the custom colors in the +generated file. For Sass files a mixin is defined, see the [high contrast override mixins section](#high-contrast-override-mixins) +for more information. Defaults to false. +* `directory` - Relative path to a directory within the project that the +generated theme file should be created in. Defaults to the project root. +* `isScss` - Whether to generate output file in Sass or CSS. Angular recommends generating a Sass +file, see the [file output section](#generated-file-output) below for more information. Defaults to +true. + +## Generated file output +The result of running the schematic is a new file with the generated custom colors. + +Angular recommendeds generating a Sass file since our theming system Sass APIs are supported and +have benefits such as error handling and relate to the [theming documentation](https://material.angular.io/guide/theming). +If there are ever changes to the theming system or system variable names, your styles will continue +to work and be supported. Color palettes get defined in the generated file that you can pass into +the `theme()` mixin in your own theme file. See the [Sass themes section](#sass-themes) for more +information. -The output of the schematic will create a file named `_theme-colors.scss` at the -specified directory or the project root with the generated palettes. The exported -palettes (`$primary-palette` and `$tertiary-palette`) can be provided to the `theme` mixin within your theme file to use the custom colors. +You can generate a CSS file which defines all the system variables directly. This allows for +applications that do not use Sass to still interact with our theming. See the [CSS themes section](#css-themes) +for more specific information. + +## Sass themes +The output of the schematic will create a file named `_theme-colors.scss` at the specified directory +or the project root. The exported palettes (`$primary-palette` and `$tertiary-palette`) can be +provided to the `theme` mixin within your theme file to use the custom colors. ```scss @use '@angular/material' as mat; -@use './path/to/my-theme'; // location of generated file +@use './path/to/_theme-colors' as my-theme; // location of generated file html { @include mat.theme( @@ -34,104 +74,151 @@ html { } ``` -## High contrast override mixins +### High contrast override mixins High contrast override theme mixins are also generated in the file if specified. These mixins override the system level variables with high contrast equivalent values from your theme. This is helpful for users who prefer more contrastful colors for either preference or accessibility reasons. -### Creating one theme for light and dark mode -As of v19, the `theme` mixin can create one theme that detects and adapts to a user if they have -light or dark theme with the [`light-dark` function](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark). +To show the high contrast values when user's specify based on their color system preferences, apply +the `high-contrast-overrides()` mixin from the generated file wrapped inside +`@media (prefers-contrast: more)` in your theme file. You can pass in `light`, `dark`, or +`color-scheme`. To see the high contrast values in your application locally, you can [use Chrome DevTools to emulate +the CSS media features](https://developer.chrome.com/docs/devtools/rendering/emulate-css). + +``` +@media (prefers-contrast: more) { + @include my-theme.high-contrast-overrides(light); +} +``` + +#### Adaptive high contrast colors for explicit light and dark themes +You can manually define the light theme and dark theme separately. This is recommended if you need +granular control over when to show each specific theme in your application. Prior to v19, this was +the only way to create light and dark themes. -Apply the `high-contrast-overrides(color-scheme)` mixin wrapped inside `@media (prefers-contrast: more)`. +In this example, the colors would automatically change between dark and dark high contrast based on +user's contrast preferences. ```scss @use '@angular/material'; @use './path/to/my-theme'; // location of generated file html { - // Must specify color-scheme for theme mixin to automatically work - color-scheme: light; - - // Create one theme that works automatically for light and dark theme + // Apply the dark theme by default @include material.theme(( color: ( primary: my-theme.$primary-palette, tertiary: my-theme.$tertiary-palette, + theme-type: dark, ), typography: Roboto, density: 0, )); - // Use high contrast values when users prefer contrast + // Use high contrast dark theme colors when users prefer contrast @media (prefers-contrast: more) { - @include my-theme.high-contrast-overrides(color-scheme); + @include my-theme.high-contrast-overrides(dark); } } ``` -### Creating separate themes for light and dark mode -You can manually define the light theme and dark theme separately. This is recommended if you need -granular control over when to show each specific theme in your application. Prior to v19, this was -the only way to create light and dark themes. +#### Adaptive high contrast colors for adaptive themes +The `theme()` mixin can create one theme that detects and adapts to a user if they have a +light or dark theme by defining `color-scheme`. See the [color-scheme documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) +and the [theming guide](https://material.angular.io/guide/theming#supporting-light-and-dark-mode) +for more information. + +Apply the `high-contrast-overrides(color-scheme)` mixin wrapped inside `@media (prefers-contrast: more)` +to apply the high contrast colors for the current color-scheme. + +In this example, the colors would automatically change between light, light high contrast, dark, and +dark high contrast based on user's preferences. ```scss @use '@angular/material'; @use './path/to/my-theme'; // location of generated file html { - // Apply the light theme by default + // Must specify color-scheme for theme mixin to automatically work + color-scheme: light dark; + + // Create one theme that works automatically for light and dark theme @include material.theme(( color: ( primary: my-theme.$primary-palette, tertiary: my-theme.$tertiary-palette, - theme-type: light, ), typography: Roboto, density: 0, )); - // Use high contrast light theme colors when users prefer contrast + // Use high contrast values when users prefer contrast @media (prefers-contrast: more) { - @include my-theme.high-contrast-overrides(light); + @include my-theme.high-contrast-overrides(color-scheme); } +} +``` - // Apply dark theme when users prefer a dark color scheme - @media (prefers-color-scheme: dark) { - @include material.theme(( - color: ( - primary: my-theme.$primary-palette, - tertiary: my-theme.$tertiary-palette, - theme-type: dark, - ), - )); - - // Use high contrast dark theme colors when users prefers a dark color scheme and contrast - @media (prefers-contrast: more) { - @include my-theme.high-contrast-overrides(dark); - } - } +## CSS themes +The output of the schematic will create a file named `theme.css` at the specified directory or the +project root. The system variables are split up by color, typography, +elevation, shape, and state variables. + +These system variables are used throughout the different components, so changing these values will +reflect across all the components. Some of the color system variables are related to other system +variables, so make sure to change corresponding system variables as well. + +### Light and dark themes +Color system variables are defined using the CSS [light-dark()](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark) +function. + +The generated CSS file sets `color-scheme` to `light` so the colors will appear as a light +theme by default. Changing the value of `color-scheme` to `dark` shows the dark version of your +application. + +To use the user's color scheme preferences automatically, set change the value of `color-scheme` to +`light-dark`. See the [color-scheme documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) +for more information. + +```css +html { + /* COLOR SYSTEM VARIABLES */ + color-scheme: light dark; /* Change to `light dark` to automatically toggle between light and dark themes. */ + + /* Primary palette variables */ + --mat-sys-primary: light-dark(#984061, #ffb0c8); + --mat-sys-on-primary: light-dark(#ffffff, #5e1133); + ... } ``` -## Options +To change the color preferences in your application locally, you can [use Chrome DevTools to emulate +the CSS media features](https://developer.chrome.com/docs/devtools/rendering/emulate-css). -### Required -* `primaryColor` - Color to use for app's primary color palette (Note: the other -palettes described in the Material 3 spec will be automatically chosen based on -your primary palette unless specified, to ensure a harmonious color combination). +### High contrast media query +High contrast values are defined in the generated CSS file if specified. These color values are +wrapped in a `@media (prefers-contrast: more)` so users only see the higher contrast version of the +colors based on their system settings. -### Optional +```css +html { + /* COLOR SYSTEM VARIABLES */ + color-scheme: light; -* `secondaryColor` - Color to use for app's secondary color palette. Defaults to -secondary color generated from Material based on the primary. -* `tertiaryColor` - Color to use for app's tertiary color palette. Defaults to -tertiary color generated from Material based on the primary. -* `neutralColor` - Color to use for app's neutral color palette. Defaults to -neutral color generated from Material based on the primary. -* `includeHighContrast` - Whether to add high contrast override mixins to generated -theme file. Developers can call the mixin when they want to show a high contrast version of their -theme. Defaults to false. -* `directory` - Relative path to a directory within the project that the -generated theme file should be created in. Defaults to the project root. + /* Primary palette variables */ + --mat-sys-primary: light-dark(#984061, #ffb0c8); + --mat-sys-on-primary: light-dark(#ffffff, #5e1133); + ... + + @media (prefers-contrast: more) { + /* Primary palette variables */ + --mat-sys-primary: light-dark(#580b2f, #ffebef); + --mat-sys-on-primary: light-dark(#ffffff, #000000); + ... + } +} +``` + +To see the high contrast values in your application locally, you can [use Chrome DevTools to emulate +the CSS media features](https://developer.chrome.com/docs/devtools/rendering/emulate-css). diff --git a/src/material/schematics/ng-generate/theme-color/index.spec.ts b/src/material/schematics/ng-generate/theme-color/index.spec.ts index de1702812123..73096a151bed 100644 --- a/src/material/schematics/ng-generate/theme-color/index.spec.ts +++ b/src/material/schematics/ng-generate/theme-color/index.spec.ts @@ -5,7 +5,7 @@ import {runfiles} from '@bazel/runfiles'; import {compileString} from 'sass'; import * as path from 'path'; import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer'; -import {generateSCSSTheme} from './index'; +import {ColorPalettes, generateSCSSTheme, getColorPalettes} from './index'; import {Schema} from './schema'; // Note: For Windows compatibility, we need to resolve the directory paths through runfiles @@ -16,7 +16,15 @@ const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir); describe('material-theme-color-schematic', () => { let runner: SchematicTestRunner; - let testM3ThemePalette: Map>; + let testM3ColorPalettes: ColorPalettes; + + async function runM3ThemeSchematic( + runner: SchematicTestRunner, + options: Schema, + ): Promise { + const app = await createTestApp(runner, {standalone: true}); + return runner.runSchematic('theme-color', options, app); + } /** Transpiles given Sass content into CSS. */ function transpileTheme(content: string): string { @@ -63,16 +71,8 @@ describe('material-theme-color-schematic', () => { ).css.toString(); } - async function runM3ThemeSchematic( - runner: SchematicTestRunner, - options: Schema, - ): Promise { - const app = await createTestApp(runner, {standalone: true}); - return runner.runSchematic('theme-color', options, app); - } - beforeEach(() => { - testM3ThemePalette = getPaletteMap(); + testM3ColorPalettes = getColorPalettes('#984061'); runner = new SchematicTestRunner( '@angular/material', runfiles.resolveWorkspaceRelative('src/material/schematics/collection.json'), @@ -91,229 +91,405 @@ describe('material-theme-color-schematic', () => { } }); - it('should generate m3 theme file', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', + describe('with scss output', async () => { + it('should generate theme file', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + }); + expect(tree.exists('_theme-colors.scss')).toBe(true); }); - expect(tree.exists('_theme-colors.scss')).toBe(true); - }); - it('should generate m3 theme file at specified path', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - directory: 'projects/', + it('should generate theme file at specified path', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + directory: 'projects/', + }); + expect(tree.exists('projects/_theme-colors.scss')).toBe(true); }); - expect(tree.exists('projects/_theme-colors.scss')).toBe(true); - }); - it('should generate m3 theme file with correct indentation and formatting', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - }); - expect(tree.readText('_theme-colors.scss')).toEqual(getTestTheme()); - }); + it('should generate theme file with correct indentation and formatting', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + }); - it('should generate themes when provided a primary color', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', + expect(tree.readText('_theme-colors.scss')).toEqual(getTestScssTheme()); }); - const generatedSCSS = tree.readText('_theme-colors.scss'); - const testSCSS = generateSCSSTheme( - testM3ThemePalette, - 'Color palettes are generated from primary: #984061', - ); + it('should generate themes when provided a primary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + }); - expect(generatedSCSS).toBe(testSCSS); - expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); - }); + const generatedSCSS = tree.readText('_theme-colors.scss'); + const testSCSS = generateSCSSTheme( + testM3ColorPalettes, + 'Color palettes are generated from primary: #984061', + ); - it('should generate themes when provided primary and secondary colors', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', + expect(generatedSCSS).toBe(testSCSS); }); - const generatedSCSS = tree.readText('_theme-colors.scss'); + it('should generate themes when provided primary and secondary colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + }); - // Change test theme palette so that secondary is the same source color as - // primary to match schematic inputs - let testPalette = testM3ThemePalette; - testPalette.set('secondary', testM3ThemePalette.get('primary')!); + const generatedSCSS = tree.readText('_theme-colors.scss'); - const testSCSS = generateSCSSTheme( - testPalette, - 'Color palettes are generated from primary: #984061, secondary: #984061', - ); + // Change test theme palette so that secondary is the same source color as + // primary to match schematic inputs + let testPalettes = testM3ColorPalettes; + testPalettes.secondary = testPalettes.primary; - expect(generatedSCSS).toBe(testSCSS); - expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); - }); + const testSCSS = generateSCSSTheme( + testPalettes, + 'Color palettes are generated from primary: #984061, secondary: #984061', + ); - it('should generate themes when provided primary, secondary, and tertiary colors', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', - tertiaryColor: '#984061', + expect(generatedSCSS).toBe(testSCSS); + expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); }); - const generatedSCSS = tree.readText('_theme-colors.scss'); + it('should generate themes when provided primary, secondary, and tertiary colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + }); - // Change test theme palette so that secondary and tertiary are the same - // source color as primary to match schematic inputs - let testPalette = testM3ThemePalette; - testPalette.set('secondary', testM3ThemePalette.get('primary')!); - testPalette.set('tertiary', testM3ThemePalette.get('primary')!); + const generatedSCSS = tree.readText('_theme-colors.scss'); - const testSCSS = generateSCSSTheme( - testPalette, - 'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061', - ); + // Change test theme palette so that secondary and tertiary are the same + // source color as primary to match schematic inputs + let testPalettes = testM3ColorPalettes; + testPalettes.secondary = testPalettes.primary; + testPalettes.tertiary = testPalettes.primary; - expect(generatedSCSS).toBe(testSCSS); - expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); - }); + const testSCSS = generateSCSSTheme( + testPalettes, + 'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061', + ); - it('should generate themes when provided a primary, secondary, tertiary, and neutral colors', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', - tertiaryColor: '#984061', - neutralColor: '#984061', + expect(generatedSCSS).toBe(testSCSS); + expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); }); - const generatedSCSS = tree.readText('_theme-colors.scss'); - - // Change test theme palette so that secondary, tertiary, and neutral are - // the same source color as primary to match schematic inputs - let testPalette = testM3ThemePalette; - testPalette.set('secondary', testM3ThemePalette.get('primary')!); - testPalette.set('tertiary', testM3ThemePalette.get('primary')!); - - // Neutral's tonal palette has additional tones as opposed to the other color palettes. - let neutralPalette = new Map(testM3ThemePalette.get('primary')!); - neutralPalette.set(4, '#26000f'); - neutralPalette.set(6, '#2f0015'); - neutralPalette.set(12, '#460022'); - neutralPalette.set(17, '#55082c'); - neutralPalette.set(22, '#631637'); - neutralPalette.set(24, '#691a3c'); - neutralPalette.set(87, '#ffcdda'); - neutralPalette.set(92, '#ffe1e8'); - neutralPalette.set(94, '#ffe8ed'); - neutralPalette.set(96, '#fff0f2'); - testPalette.set('neutral', neutralPalette); - - const testSCSS = generateSCSSTheme( - testPalette, - 'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061, neutral: #984061', - ); + it('should generate themes when provided a primary, secondary, tertiary, and neutral colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + neutralColor: '#984061', + }); - expect(generatedSCSS).toBe(testSCSS); - expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); - }); + const generatedSCSS = tree.readText('_theme-colors.scss'); - it('should be able to generate high contrast overrides mixin', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - includeHighContrast: true, + // Change test theme palette so that secondary, tertiary, and neutral are + // the same source color as primary to match schematic inputs + let testPalettes = testM3ColorPalettes; + testPalettes.secondary = testPalettes.primary; + testPalettes.tertiary = testPalettes.primary; + testPalettes.neutral = testPalettes.primary; + + const testSCSS = generateSCSSTheme( + testPalettes, + 'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061, neutral: #984061', + ); + + expect(generatedSCSS).toBe(testSCSS); + expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS)); }); - const generatedSCSS = tree.readText('_theme-colors.scss'); + describe('and with high contrast overrides', () => { + it('should be able to generate high contrast overrides mixin', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + includeHighContrast: true, + }); + + const generatedSCSS = tree.readText('_theme-colors.scss'); + + expect(generatedSCSS).toContain(`@mixin high-contrast-overrides`); + }); + + it('should be able to generate high contrast themes overrides when provided a primary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + includeHighContrast: true, + }); + + const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + + // Check a system variable from each color palette for their high contrast light theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #45212d`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #4d1f00`); + expect(generatedCSS).toContain(`--mat-sys-error: #600004`); + expect(generatedCSS).toContain(`--mat-sys-surface: #fff8f8`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: #f6dce2`); + + // Check a system variable from each color palette for their high contrast dark theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffece4`); + expect(generatedCSS).toContain(`--mat-sys-error: #ffece9`); + expect(generatedCSS).toContain(`--mat-sys-surface: #191113`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: #534247`); + }); + + it('should be able to generate high contrast themes overrides when provided a primary and secondary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + includeHighContrast: true, + }); + + const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + + // Check a system variable from each color palette for their high contrast light theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); - expect(generatedSCSS).toContain(`@mixin high-contrast-overrides`); + // Check a system variable from each color palette for their high contrast dark theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); + }); + + it('should be able to generate high contrast themes overrides when provided primary, secondary, and tertiary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + includeHighContrast: true, + }); + + const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + + // Check a system variable from each color palette for their high contrast light theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`); + + // Check a system variable from each color palette for their high contrast dark theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`); + }); + + it('should be able to generate high contrast themes overrides when provided primary, secondary, tertiary, and neutral color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette + includeHighContrast: true, + }); + + const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + + // Check a system variable from each color palette for their high contrast light theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`); + expect(generatedCSS).toContain(`--mat-sys-surface-bright: #f9f9f9`); + + // Check a system variable from each color palette for their high contrast dark theme value + expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`); + expect(generatedCSS).toContain(`--mat-sys-surface-bright: #4f5051`); + }); + }); }); - it('should be able to generate high contrast themes overrides when provided a primary color', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - includeHighContrast: true, + describe('with CSS output', async () => { + it('should generate m3 theme CSS file', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + isScss: false, + }); + expect(tree.exists('theme.css')).toBe(true); }); - const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); - - // Check a system variable from each color palette for their high contrast light theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #45212d`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #4d1f00`); - expect(generatedCSS).toContain(`--mat-sys-error: #600004`); - expect(generatedCSS).toContain(`--mat-sys-surface: #fff8f8`); - expect(generatedCSS).toContain(`--mat-sys-outline: #37282c`); - - // Check a system variable from each color palette for their high contrast dark theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffece4`); - expect(generatedCSS).toContain(`--mat-sys-error: #ffece9`); - expect(generatedCSS).toContain(`--mat-sys-surface: #191113`); - expect(generatedCSS).toContain(`--mat-sys-outline: #ffebef`); - }); + it('should generate m3 theme CSS file at specified path', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + directory: 'projects/', + isScss: false, + }); + expect(tree.exists('projects/theme.css')).toBe(true); + }); - it('should be able to generate high contrast themes overrides when provided a primary and secondary color', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', - includeHighContrast: true, + it('should generate m3 theme CSS file with correct indentation and formatting', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + isScss: false, + includeHighContrast: true, + }); + + expect(tree.readText('theme.css')).toEqual(getTestCssTheme()); }); - const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + it('should generate CSS system variables when provided a primary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + isScss: false, + }); - // Check a system variable from each color palette for their high contrast light theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); + const generatedCSS = tree.readText('theme.css'); - // Check a system variable from each color palette for their high contrast dark theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); - }); + // Check a system variable from each color palette for their light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#7e525f, #efb8c7)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#974810, #ffb68e)`); + expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#ba1a1a, #ffb4ab)`); + expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #191113)`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`); + }); - it('should be able to generate high contrast themes overrides when provided primary, secondary, and tertiary color', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', - tertiaryColor: '#984061', - includeHighContrast: true, + it('should generate CSS system variables when provided a primary and secondary colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + isScss: false, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#974810, #ffb68e)`); + expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#ba1a1a, #ffb4ab)`); + expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #191113)`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`); }); - const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + it('should generate CSS system variables when provided a primary, secondary, and tertiary colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + isScss: false, + }); - // Check a system variable from each color palette for their high contrast light theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`); + const generatedCSS = tree.readText('theme.css'); - // Check a system variable from each color palette for their high contrast dark theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`); - }); + // Check a system variable from each color palette for their light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#ba1a1a, #ffb4ab)`); + expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #191113)`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`); + }); - it('should be able to generate high contrast themes overrides when provided primary, secondary, tertiary, and neutral color', async () => { - const tree = await runM3ThemeSchematic(runner, { - primaryColor: '#984061', - secondaryColor: '#984061', - tertiaryColor: '#984061', - neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette - includeHighContrast: true, + it('should generate CSS system variables when provided a primary, secondary, tertiary, and neutral colors', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + neutralColor: '#984061', + isScss: false, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#984061, #ffb0c8)`); + expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#ba1a1a, #ffb4ab)`); + expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #2f0015);`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`); }); - const generatedCSS = transpileTheme(tree.readText('_theme-colors.scss')); + describe('and with high contrast overrides', () => { + it('should generate high contrast system variables', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + isScss: false, + includeHighContrast: true, + }); - // Check a system variable from each color palette for their high contrast light theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #580b2f`); - expect(generatedCSS).toContain(`--mat-sys-surface-bright: #f9f9f9`); + const generatedCSS = tree.readText('theme.css'); + expect(generatedCSS).toContain(`@media (prefers-contrast: more) {`); + }); + + it('should generate high contrast system variables when provided a primary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + isScss: false, + includeHighContrast: true, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their high contrast light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#45212d, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#4d1f00, #ffece4)`); + expect(generatedCSS).toContain(`--mat-sys-error: light-dark(#600004, #ffece9)`); + expect(generatedCSS).toContain(`--mat-sys-surface: light-dark(#fff8f8, #191113)`); + expect(generatedCSS).toContain(`--mat-sys-surface-variant: light-dark(#f6dce2, #534247)`); + }); - // Check a system variable from each color palette for their high contrast dark theme value - expect(generatedCSS).toContain(`--mat-sys-primary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-secondary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-tertiary: #ffebef`); - expect(generatedCSS).toContain(`--mat-sys-surface-bright: #4f5051`); + it('should generate high contrast system variables when provided a primary and secondary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + isScss: false, + includeHighContrast: true, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their high contrast light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#580b2f, #ffebef)`); + }); + + it('should generate high contrast system variables when provided primary, secondary, and tertiary color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + isScss: false, + includeHighContrast: true, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their high contrast light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#580b2f, #ffebef)`); + }); + + it('should generate high contrast system variables when provided primary, secondary, tertiary, and neutral color', async () => { + const tree = await runM3ThemeSchematic(runner, { + primaryColor: '#984061', + secondaryColor: '#984061', + tertiaryColor: '#984061', + neutralColor: '#dfdfdf', // Different color since #984061 does not change the tonal palette + isScss: false, + includeHighContrast: true, + }); + + const generatedCSS = tree.readText('theme.css'); + + // Check a system variable from each color palette for their high contrast light dark value + expect(generatedCSS).toContain(`--mat-sys-primary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-secondary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-tertiary: light-dark(#580b2f, #ffebef)`); + expect(generatedCSS).toContain(`--mat-sys-surface-bright: light-dark(#f9f9f9, #4f5051)`); + }); + }); }); }); -function getTestTheme() { +function getTestScssTheme(): string { return `// This file was generated by running 'ng generate @angular/material:theme-color'. // Proceed with caution if making changes to this file. @@ -453,146 +629,305 @@ $primary-palette: map.merge(map.get($_palettes, primary), $_rest); $tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);`; } -function getPaletteMap() { - // Hue maps created from https://m3.material.io/theme-builder#/custom (using - // #984061 as source color). Not using predefined M3 palettes since some neutral - // hues are slightly off from generated theme. - return new Map([ - [ - 'primary', - new Map([ - [0, '#000000'], - [10, '#3e001d'], - [20, '#5e1133'], - [25, '#6c1d3e'], - [30, '#7b2949'], - [35, '#893455'], - [40, '#984061'], - [50, '#b6587a'], - [60, '#d57194'], - [70, '#f48bae'], - [80, '#ffb0c8'], - [90, '#ffd9e2'], - [95, '#ffecf0'], - [98, '#fff8f8'], - [99, '#fffbff'], - [100, '#ffffff'], - ]), - ], - [ - 'secondary', - new Map([ - [0, '#000000'], - [10, '#31101d'], - [20, '#4a2531'], - [25, '#56303c'], - [30, '#633b48'], - [35, '#704653'], - [40, '#7e525f'], - [50, '#996a78'], - [60, '#b58392'], - [70, '#d29dac'], - [80, '#efb8c7'], - [90, '#ffd9e2'], - [95, '#ffecf0'], - [98, '#fff8f8'], - [99, '#fffbff'], - [100, '#ffffff'], - ]), - ], - [ - 'tertiary', - new Map([ - [0, '#000000'], - [10, '#331200'], - [20, '#532200'], - [25, '#642a00'], - [30, '#763300'], - [35, '#883d03'], - [40, '#974810'], - [50, '#b66028'], - [60, '#d6783e'], - [70, '#f69256'], - [80, '#ffb68e'], - [90, '#ffdbc9'], - [95, '#ffede5'], - [98, '#fff8f6'], - [99, '#fffbff'], - [100, '#ffffff'], - ]), - ], - [ - 'neutral', - new Map([ - [0, '#000000'], - [10, '#22191c'], - [20, '#372e30'], - [25, '#43393b'], - [30, '#4f4446'], - [35, '#5b5052'], - [40, '#675b5e'], - [50, '#807477'], - [60, '#9b8d90'], - [70, '#b6a8aa'], - [80, '#d2c3c5'], - [90, '#efdfe1'], - [95, '#fdedef'], - [98, '#fff8f8'], - [99, '#fffbff'], - [100, '#ffffff'], - [4, '#140c0e'], - [6, '#191113'], - [12, '#261d20'], - [17, '#31282a'], - [22, '#3c3235'], - [24, '#413739'], - [87, '#e6d6d9'], - [92, '#f5e4e7'], - [94, '#faeaed'], - [96, '#fff0f2'], - ]), - ], - [ - 'neutral-variant', - new Map([ - [0, '#000000'], - [10, '#25181c'], - [20, '#3c2c31'], - [25, '#47373b'], - [30, '#534247'], - [35, '#604e52'], - [40, '#6c5a5e'], - [50, '#867277'], - [60, '#a18b90'], - [70, '#bca5ab'], - [80, '#d9c0c6'], - [90, '#f6dce2'], - [95, '#ffecf0'], - [98, '#fff8f8'], - [99, '#fffbff'], - [100, '#ffffff'], - ]), - ], - [ - 'error', - new Map([ - [0, '#000000'], - [10, '#410002'], - [20, '#690005'], - [25, '#7e0007'], - [30, '#93000a'], - [35, '#a80710'], - [40, '#ba1a1a'], - [50, '#de3730'], - [60, '#ff5449'], - [70, '#ff897d'], - [80, '#ffb4ab'], - [90, '#ffdad6'], - [95, '#ffedea'], - [98, '#fff8f7'], - [99, '#fffbff'], - [100, '#ffffff'], - ]), - ], - ]); +function getTestCssTheme(): string { + return `/* Note: Color palettes are generated from primary: #984061 */ +html { + /* COLOR SYSTEM VARIABLES */ + color-scheme: light; + + /* Primary palette variables */ + --mat-sys-primary: light-dark(#984061, #ffb0c8); + --mat-sys-on-primary: light-dark(#ffffff, #5e1133); + --mat-sys-primary-container: light-dark(#ffd9e2, #7b2949); + --mat-sys-on-primary-container: light-dark(#3e001d, #ffd9e2); + --mat-sys-inverse-primary: light-dark(#ffb0c8, #984061); + --mat-sys-primary-fixed: light-dark(#ffd9e2, #ffd9e2); + --mat-sys-primary-fixed-dim: light-dark(#ffb0c8, #ffb0c8); + --mat-sys-on-primary-fixed: light-dark(#3e001d, #3e001d); + --mat-sys-on-primary-fixed-variant: light-dark(#7b2949, #7b2949); + + /* Secondary palette variables */ + --mat-sys-secondary: light-dark(#7e525f, #efb8c7); + --mat-sys-on-secondary: light-dark(#ffffff, #4a2531); + --mat-sys-secondary-container: light-dark(#ffd9e2, #633b48); + --mat-sys-on-secondary-container: light-dark(#31101d, #ffd9e2); + --mat-sys-secondary-fixed: light-dark(#ffd9e2, #ffd9e2); + --mat-sys-secondary-fixed-dim: light-dark(#efb8c7, #efb8c7); + --mat-sys-on-secondary-fixed: light-dark(#31101d, #31101d); + --mat-sys-on-secondary-fixed-variant: light-dark(#633b48, #633b48); + + /* Tertiary palette variables */ + --mat-sys-tertiary: light-dark(#974810, #ffb68e); + --mat-sys-on-tertiary: light-dark(#ffffff, #532200); + --mat-sys-tertiary-container: light-dark(#ffdbc9, #763300); + --mat-sys-on-tertiary-container: light-dark(#331200, #ffdbc9); + --mat-sys-tertiary-fixed: light-dark(#ffdbc9, #ffdbc9); + --mat-sys-tertiary-fixed-dim: light-dark(#ffb68e, #ffb68e); + --mat-sys-on-tertiary-fixed: light-dark(#331200, #331200); + --mat-sys-on-tertiary-fixed-variant: light-dark(#763300, #763300); + + /* Neutral palette variables */ + --mat-sys-background: light-dark(#fff8f8, #191113); + --mat-sys-on-background: light-dark(#22191c, #efdfe1); + --mat-sys-surface: light-dark(#fff8f8, #191113); + --mat-sys-surface-dim: light-dark(#e6d6d9, #191113); + --mat-sys-surface-bright: light-dark(#fff8f8, #413739); + --mat-sys-surface-container-lowest: light-dark(#ffffff, #140c0e); + --mat-sys-surface-container: light-dark(#faeaed, #261d20); + --mat-sys-surface-container-high: light-dark(#f5e4e7, #31282a); + --mat-sys-surface-container-highest: light-dark(#efdfe1, #3c3235); + --mat-sys-on-surface: light-dark(#22191c, #efdfe1); + --mat-sys-shadow: light-dark(#000000, #000000); + --mat-sys-scrim: light-dark(#000000, #000000); + --mat-sys-surface-tint: light-dark(#984061, #ffb0c8); + --mat-sys-inverse-surface: light-dark(#372e30, #efdfe1); + --mat-sys-inverse-on-surface: light-dark(#fdedef, #372e30); + --mat-sys-outline: light-dark(#867277, #a18b90); + --mat-sys-outline-variant: light-dark(#d9c0c6, #534247); + --mat-sys-neutral10: light-dark(#22191c, #22191c); /* Variable used for the form field native select option text color */ + + /* Error palette variables */ + --mat-sys-error: light-dark(#ba1a1a, #ffb4ab); + --mat-sys-on-error: light-dark(#ffffff, #690005); + --mat-sys-error-container: light-dark(#ffdad6, #93000a); + --mat-sys-on-error-container: light-dark(#410002, #ffdad6); + + /* Neutral variant palette variables */ + --mat-sys-surface-variant: light-dark(#f6dce2, #534247); + --mat-sys-on-surface-variant: light-dark(#534247, #d9c0c6); + --mat-sys-neutral-variant20: light-dark(#3c2c31, #3c2c31); /* Variable used for the sidenav scrim (container background shadow when opened) */ + + /* TYPOGRAPHY SYSTEM VARIABLES */ + + /* Typography variables. Only used in the different typescale system variables. */ + --mat-sys-brand-font-family: Roboto; /* The font-family to use for brand text. */ + --mat-sys-plain-font-family: Roboto; /* The font-family to use for plain text. */ + --mat-sys-bold-font-weight: 700; /* The font-weight to use for bold text. */ + --mat-sys-medium-font-weight: 500; /* The font-weight to use for medium text. */ + --mat-sys-regular-font-weight: 400; /* The font-weight to use for regular text. */ + + /* Typescale variables. */ + /* Warning: Risk of reduced fidelity from using the composite typography tokens (ex. --mat-sys-body-large) since + tracking cannot be represented in the "font" property shorthand. Consider using the discrete properties instead. */ + --mat-sys-body-large: var(--mat-sys-body-large-weight) var(--mat-sys-body-large-size) / var(--mat-sys-body-large-line-height) var(--mat-sys-body-large-font); + --mat-sys-body-large-font: var(--mat-sys-plain-font-family); + --mat-sys-body-large-line-height: 1.5rem; + --mat-sys-body-large-size: 1rem; + --mat-sys-body-large-tracking: 0.031rem; + --mat-sys-body-large-weight: var(--mat-sys-regular-font-weight); + + /* Body medium typescale */ + --mat-sys-body-medium: var(--mat-sys-body-medium-weight) var(--mat-sys-body-medium-size) / var(--mat-sys-body-medium-line-height) var(--mat-sys-body-medium-font); + --mat-sys-body-medium-font: var(--mat-sys-plain-font-family); + --mat-sys-body-medium-line-height: 1.25rem; + --mat-sys-body-medium-size: 0.875rem; + --mat-sys-body-medium-tracking: 0.016rem; + --mat-sys-body-medium-weight: var(--mat-sys-regular-font-weight); + + /* Body small typescale */ + --mat-sys-body-small: var(--mat-sys-body-small-weight) var(--mat-sys-body-small-size) / var(--mat-sys-body-small-line-height) var(--mat-sys-body-small-font); + --mat-sys-body-small-font: var(--mat-sys-plain-font-family); + --mat-sys-body-small-line-height: 1rem; + --mat-sys-body-small-size: 0.75rem; + --mat-sys-body-small-tracking: 0.025rem; + --mat-sys-body-small-weight: var(--mat-sys-regular-font-weight); + + /* Display large typescale */ + --mat-sys-display-large: var(--mat-sys-display-large-weight) var(--mat-sys-display-large-size) / var(--mat-sys-display-large-line-height) var(--mat-sys-display-large-font); + --mat-sys-display-large-font: var(--mat-sys-brand-font-family); + --mat-sys-display-large-line-height: 4rem; + --mat-sys-display-large-size: 3.562rem; + --mat-sys-display-large-tracking: -0.016rem; + --mat-sys-display-large-weight: var(--mat-sys-regular-font-weight); + + /* Display medium typescale */ + --mat-sys-display-medium: var(--mat-sys-display-medium-weight) var(--mat-sys-display-medium-size) / var(--mat-sys-display-medium-line-height) var(--mat-sys-display-medium-font); + --mat-sys-display-medium-font: var(--mat-sys-brand-font-family); + --mat-sys-display-medium-line-height: 3.25rem; + --mat-sys-display-medium-size: 2.812rem; + --mat-sys-display-medium-tracking: 0; + --mat-sys-display-medium-weight: var(--mat-sys-regular-font-weight); + + /* Display small typescale */ + --mat-sys-display-small: var(--mat-sys-display-small-weight) var(--mat-sys-display-small-size) / var(--mat-sys-display-small-line-height) var(--mat-sys-display-small-font); + --mat-sys-display-small-font: var(--mat-sys-brand-font-family); + --mat-sys-display-small-line-height: 2.75rem; + --mat-sys-display-small-size: 2.25rem; + --mat-sys-display-small-tracking: 0; + --mat-sys-display-small-weight: var(--mat-sys-regular-font-weight); + + /* Headline large typescale */ + --mat-sys-headline-large: var(--mat-sys-headline-large-weight) var(--mat-sys-headline-large-size) / var(--mat-sys-headline-large-line-height) var(--mat-sys-headline-large-font); + --mat-sys-headline-large-font: var(--mat-sys-brand-font-family); + --mat-sys-headline-large-line-height: 2.5rem; + --mat-sys-headline-large-size: 2rem; + --mat-sys-headline-large-tracking: 0; + --mat-sys-headline-large-weight: var(--mat-sys-regular-font-weight); + + /* Headline medium typescale */ + --mat-sys-headline-medium: var(--mat-sys-headline-medium-weight) var(--mat-sys-headline-medium-size) / var(--mat-sys-headline-medium-line-height) var(--mat-sys-headline-medium-font); + --mat-sys-headline-medium-font: var(--mat-sys-brand-font-family); + --mat-sys-headline-medium-line-height: 2.25rem; + --mat-sys-headline-medium-size: 1.75rem; + --mat-sys-headline-medium-tracking: 0; + --mat-sys-headline-medium-weight: var(--mat-sys-regular-font-weight); + + /* Headline small typescale */ + --mat-sys-headline-small: var(--mat-sys-headline-small-weight) var(--mat-sys-headline-small-size) / var(--mat-sys-headline-small-line-height) var(--mat-sys-headline-small-font); + --mat-sys-headline-small-font: var(--mat-sys-brand-font-family); + --mat-sys-headline-small-line-height: 2rem; + --mat-sys-headline-small-size: 1.5rem; + --mat-sys-headline-small-tracking: 0; + --mat-sys-headline-small-weight: var(--mat-sys-regular-font-weight); + + /* Label large typescale */ + --mat-sys-label-large: var(--mat-sys-label-large-weight) var(--mat-sys-label-large-size) / var(--mat-sys-label-large-line-height) var(--mat-sys-label-large-font); + --mat-sys-label-large-font: var(--mat-sys-plain-font-family); + --mat-sys-label-large-line-height: 1.25rem; + --mat-sys-label-large-size: 0.875rem; + --mat-sys-label-large-tracking: 0.006rem; + --mat-sys-label-large-weight: var(--mat-sys-medium-font-weight); + --mat-sys-label-large-weight-prominent: var(--mat-sys-bold-font-weight); + + /* Label medium typescale */ + --mat-sys-label-medium: var(--mat-sys-label-medium-weight) var(--mat-sys-label-medium-size) / var(--mat-sys-label-medium-line-height) var(--mat-sys-label-medium-font); + --mat-sys-label-medium-font: var(--mat-sys-plain-font-family); + --mat-sys-label-medium-line-height: 1rem; + --mat-sys-label-medium-size: 0.75rem; + --mat-sys-label-medium-tracking: 0.031rem; + --mat-sys-label-medium-weight: var(--mat-sys-medium-font-weight); + --mat-sys-label-medium-weight-prominent: var(--mat-sys-bold-font-weight); + + /* Label small typescale */ + --mat-sys-label-small: var(--mat-sys-label-small-weight) var(--mat-sys-label-small-size) / var(--mat-sys-label-small-line-height) var(--mat-sys-label-small-font); + --mat-sys-label-small-font: var(--mat-sys-plain-font-family); + --mat-sys-label-small-line-height: 1rem; + --mat-sys-label-small-size: 0.688rem; + --mat-sys-label-small-tracking: 0.031rem; + --mat-sys-label-small-weight: var(--mat-sys-medium-font-weight); + + /* Title large typescale */ + --mat-sys-title-large: var(--mat-sys-title-large-weight) var(--mat-sys-title-large-size) / var(--mat-sys-title-large-line-height) var(--mat-sys-title-large-font); + --mat-sys-title-large-font: var(--mat-sys-brand-font-family); + --mat-sys-title-large-line-height: 1.75rem; + --mat-sys-title-large-size: 1.375rem; + --mat-sys-title-large-tracking: 0; + --mat-sys-title-large-weight: var(--mat-sys-regular-font-weight); + + /* Title medium typescale */ + --mat-sys-title-medium: var(--mat-sys-title-medium-weight) var(--mat-sys-title-medium-size) / var(--mat-sys-title-medium-line-height) var(--mat-sys-title-medium-font); + --mat-sys-title-medium-font: var(--mat-sys-plain-font-family); + --mat-sys-title-medium-line-height: 1.5rem; + --mat-sys-title-medium-size: 1rem; + --mat-sys-title-medium-tracking: 0.009rem; + --mat-sys-title-medium-weight: var(--mat-sys-medium-font-weight); + + /* Title small typescale */ + --mat-sys-title-small: var(--mat-sys-title-small-weight) var(--mat-sys-title-small-size) / var(--mat-sys-title-small-line-height) var(--mat-sys-title-small-font); + --mat-sys-title-small-font: var(--mat-sys-plain-font-family); + --mat-sys-title-small-line-height: 1.25rem; + --mat-sys-title-small-size: 0.875rem; + --mat-sys-title-small-tracking: 0.006rem; + --mat-sys-title-small-weight: var(--mat-sys-medium-font-weight); + + /* ELEVATION SYSTEM VARIABLES */ + + /* Box shadow colors. Only used in the elevation level system variables. */ + --mat-sys-umbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 80%); + --mat-sys-penumbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 86%); + --mat-sys-ambient-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 88%); + + /* Elevation level system variables. These are used as the value for box-shadow CSS property. */ + --mat-sys-level0: 0px 0px 0px 0px var(--mat-sys-umbra-color), 0px 0px 0px 0px var(--mat-sys-penumbra-color), 0px 0px 0px 0px var(--mat-sys-ambient-color); + --mat-sys-level1: 0px 2px 1px -1px var(--mat-sys-umbra-color), 0px 1px 1px 0px var(--mat-sys-penumbra-color), 0px 1px 3px 0px var(--mat-sys-ambient-color); + --mat-sys-level2: 0px 3px 3px -2px var(--mat-sys-umbra-color), 0px 3px 4px 0px var(--mat-sys-penumbra-color), 0px 1px 8px 0px var(--mat-sys-ambient-color); + --mat-sys-level3: 0px 3px 5px -1px var(--mat-sys-umbra-color), 0px 6px 10px 0px var(--mat-sys-penumbra-color), 0px 1px 18px 0px var(--mat-sys-ambient-color); + --mat-sys-level4: 0px 5px 5px -3px var(--mat-sys-umbra-color), 0px 8px 10px 1px var(--mat-sys-penumbra-color), 0px 3px 14px 2px var(--mat-sys-ambient-color); + --mat-sys-level5: 0px 7px 8px -4px var(--mat-sys-umbra-color), 0px 12px 17px 2px var(--mat-sys-penumbra-color), 0px 5px 22px 4px var(--mat-sys-ambient-color); + + /* SHAPE SYSTEM VARIABLES */ + --mat-sys-corner-extra-large: 28px; + --mat-sys-corner-extra-large-top: 28px 28px 0 0; + --mat-sys-corner-extra-small: 4px; + --mat-sys-corner-extra-small-top: 4px 4px 0 0; + --mat-sys-corner-full: 9999px; + --mat-sys-corner-large: 16px; + --mat-sys-corner-large-end: 0 16px 16px 0; + --mat-sys-corner-large-start: 16px 0 0 16px; + --mat-sys-corner-large-top: 16px 16px 0 0; + --mat-sys-corner-medium: 12px; + --mat-sys-corner-none: 0; + --mat-sys-corner-small: 8px; + + /* STATE SYSTEM VARIABLES */ + --mat-sys-dragged-state-layer-opacity: 0.16; + --mat-sys-focus-state-layer-opacity: 0.12; + --mat-sys-hover-state-layer-opacity: 0.08; + --mat-sys-pressed-state-layer-opacity: 0.12; + + @media (prefers-contrast: more) { + /* Primary palette variables */ + --mat-sys-primary: light-dark(#580b2f, #ffebef); + --mat-sys-on-primary: light-dark(#ffffff, #000000); + --mat-sys-primary-container: light-dark(#7e2b4c, #ffabc5); + --mat-sys-on-primary-container: light-dark(#ffffff, #20000c); + --mat-sys-inverse-primary: light-dark(#ffb0c8, #7c2a4b); + --mat-sys-primary-fixed: light-dark(#7e2b4c, #ffd9e2); + --mat-sys-primary-fixed-dim: light-dark(#611335, #ffb0c8); + --mat-sys-on-primary-fixed: light-dark(#ffffff, #000000); + --mat-sys-on-primary-fixed-variant: light-dark(#ffffff, #2b0013); + + /* Secondary palette variables */ + --mat-sys-secondary: light-dark(#45212d, #ffebef); + --mat-sys-on-secondary: light-dark(#ffffff, #000000); + --mat-sys-secondary-container: light-dark(#663d4a, #ebb4c3); + --mat-sys-on-secondary-container: light-dark(#ffffff, #1d020c); + --mat-sys-secondary-fixed: light-dark(#663d4a, #ffd9e2); + --mat-sys-secondary-fixed-dim: light-dark(#4c2734, #efb8c7); + --mat-sys-on-secondary-fixed: light-dark(#ffffff, #000000); + --mat-sys-on-secondary-fixed-variant: light-dark(#ffffff, #240612); + + /* Tertiary palette variables */ + --mat-sys-tertiary: light-dark(#4d1f00, #ffece4); + --mat-sys-on-tertiary: light-dark(#4d1f00, #ffece4); + --mat-sys-tertiary-container: light-dark(#7a3500, #ffb184); + --mat-sys-on-tertiary-container: light-dark(#ffffff, #190600); + --mat-sys-tertiary-fixed: light-dark(#7a3500, #ffdbc9); + --mat-sys-tertiary-fixed-dim: light-dark(#572400, #ffb68e); + --mat-sys-on-tertiary-fixed: light-dark(#ffffff, #000000); + --mat-sys-on-tertiary-fixed-variant: light-dark(#ffffff, #220a00); + + /* Neutral palette variables */ + --mat-sys-background: light-dark(#fff8f8, #191113); + --mat-sys-on-background: light-dark(#22191c, #efdfe1); + --mat-sys-surface: light-dark(#fff8f8, #191113); + --mat-sys-surface-dim: light-dark(#c4b5b8, #191113); + --mat-sys-surface-bright: light-dark(#fff8f8, #584d50); + --mat-sys-surface-container-lowest: light-dark(#ffffff, #000000); + --mat-sys-surface-container: light-dark(#efdfe1, #372e30); + --mat-sys-surface-container-high: light-dark(#e0d1d3, #43393b); + --mat-sys-surface-container-highest: light-dark(#d2c3c5, #4f4446); + --mat-sys-on-surface: light-dark(#000000, #ffffff); + --mat-sys-shadow: light-dark(#000000, #000000); + --mat-sys-scrim: light-dark(#000000, #000000); + --mat-sys-surface-tint: light-dark(#984061, #ffb0c8); + --mat-sys-inverse-surface: light-dark(#372e30, #efdfe1); + --mat-sys-inverse-on-surface: light-dark(#ffffff, #000000); + --mat-sys-outline: light-dark(#37282c, #ffebef); + --mat-sys-outline-variant: light-dark(#564549, #d5bdc2); + --mat-sys-neutral10: light-dark(#22191c, #22191c); /* Variable used for the form field native select option text color */ + + /* Error palette variables */ + --mat-sys-error: light-dark(#600004, #ffece9); + --mat-sys-on-error: light-dark(#ffffff, #000000); + --mat-sys-error-container: light-dark(#98000a, #ffaea4); + --mat-sys-on-error-container: light-dark(#ffffff, #220001); + + /* Neutral variant palette variables */ + --mat-sys-surface-variant: light-dark(#f6dce2, #534247); + --mat-sys-on-surface-variant: light-dark(#000000, #ffffff); + --mat-sys-neutral-variant20: light-dark(#3c2c31, #3c2c31); /* Variable used for the sidenav scrim (container background shadow when opened) */ + } +} +`; } diff --git a/src/material/schematics/ng-generate/theme-color/index.ts b/src/material/schematics/ng-generate/theme-color/index.ts index 1fa5af81ea3f..ce722dc97c7e 100644 --- a/src/material/schematics/ng-generate/theme-color/index.ts +++ b/src/material/schematics/ng-generate/theme-color/index.ts @@ -42,6 +42,32 @@ const NEUTRAL_HUES = new Map([ // palette's 94 tone). https://m3.material.io/styles/color/static/baseline const NEUTRAL_HUE_TONES = [...HUE_TONES, ...NEUTRAL_HUES.keys()]; +export interface ColorPalettes { + primary: TonalPalette; + secondary: TonalPalette; + tertiary: TonalPalette; + neutral: TonalPalette; + neutralVariant: TonalPalette; + error: TonalPalette; +} + +/** + * Gets Hct representation of Hex color. + * @param color Hex color. + * @returns Hct color. + */ +export function getHctFromHex(color: string): Hct { + try { + return Hct.fromInt(argbFromHex(color)); + } catch (e) { + throw new Error( + 'Cannot parse the specified color ' + + color + + '. Please verify it is a hex color (ex. #ffffff or ffffff).', + ); + } +} + /** * Gets color tonal palettes generated by Material from the provided color. * @param primaryPalette Tonal palette that represents primary. @@ -54,7 +80,7 @@ const NEUTRAL_HUE_TONES = [...HUE_TONES, ...NEUTRAL_HUES.keys()]; * and 1 represents high contrast. * @returns Dynamic scheme for provided theme and contrast level */ -function getMaterialDynamicScheme( +export function getMaterialDynamicScheme( primaryPalette: TonalPalette, secondaryPalette: TonalPalette, tertiaryPalette: TonalPalette, @@ -77,42 +103,77 @@ function getMaterialDynamicScheme( } /** - * Gets the scss representation of the provided color palettes. - * @param colorPalettes Map of colors and their hue tones and values. - * @returns String of the color palettes scss. + * Gets all the color palettes. + * + * @param primaryColor Hex color that represents the primary palette. + * @param secondaryColor Hex color that represents the secondary palette. + * @param tertiaryColor Hex color that represents the tertiary palette. + * @param neutralColor Hex color that represents the neutral palette. + * @returns Object with all the M3 color palettes */ -function getColorPalettesSCSS(colorPalettes: Map>): string { - let scss = '(\n'; - for (const [variant, palette] of colorPalettes!.entries()) { - scss += ' ' + variant + ': (\n'; - for (const [key, value] of palette.entries()) { - scss += ' ' + key + ': ' + value + ',\n'; - } - scss += ' ),\n'; +export function getColorPalettes( + primaryColor: string, + secondaryColor?: string, + tertiaryColor?: string, + neutralColor?: string, +): ColorPalettes { + // Create tonal palettes for each color and custom color overrides if applicable. Used for both + // standard contrast and high contrast schemes since they share the same tonal palettes. + // The math to generate the palettes follows how palettes are generated for SchemeFidelity + // (https://github.com/material-foundation/material-color-utilities/blob/main/typescript/scheme/scheme_fidelity.ts). + // Cannot create object directly since we allow users to enter custom colors for palettes and + // palettes are readonly for a DynamicScheme. + const primaryColorHct = getHctFromHex(primaryColor); + const primaryPalette = TonalPalette.fromHct(primaryColorHct); + + let secondaryPalette; + if (secondaryColor) { + secondaryPalette = TonalPalette.fromHct(getHctFromHex(secondaryColor)); + } else { + secondaryPalette = TonalPalette.fromHueAndChroma( + primaryColorHct.hue, + Math.max(primaryColorHct.chroma - 32.0, primaryColorHct.chroma * 0.5), + ); } - scss += ');'; - return scss; -} -/** - * Gets map of all the color tonal palettes with their tones and colors from provided palettes. - * @param primaryPalette Tonal palette that represents primary. - * @param secondaryPalette Tonal palette that represents secondary. - * @param tertiaryPalette Tonal palette that represents tertiary. - * @param neutralPalette Tonal palette that represents neutral. - * @param neutralVariantPalette Tonal palette that represents neutral variant. - * @param errorPalette Tonal palette that represents error. - * @returns Map with the colors and their hue tones and values. - */ -function getMapFromColorTonalPalettes( - primaryPalette: TonalPalette, - secondaryPalette: TonalPalette, - tertiaryPalette: TonalPalette, - neutralPalette: TonalPalette, - neutralVariantPalette: TonalPalette, - errorPalette: TonalPalette, -) { - const tonalPalettes = { + let tertiaryPalette; + if (tertiaryColor) { + tertiaryPalette = TonalPalette.fromHct(getHctFromHex(tertiaryColor)); + } else { + tertiaryPalette = TonalPalette.fromInt( + DislikeAnalyzer.fixIfDisliked( + new TemperatureCache(primaryColorHct).analogous(3, 6)[2], + ).toInt(), + ); + } + + let neutralPalette; + if (neutralColor) { + neutralPalette = TonalPalette.fromHct(getHctFromHex(neutralColor)); + } else { + neutralPalette = TonalPalette.fromHueAndChroma( + primaryColorHct.hue, + primaryColorHct.chroma / 8.0, + ); + } + + const neutralVariantPalette = TonalPalette.fromHueAndChroma( + primaryColorHct.hue, + primaryColorHct.chroma / 8.0 + 4.0, + ); + + // Need to create color scheme to get generated error tonal palette. + const errorPalette = getMaterialDynamicScheme( + primaryPalette, + secondaryPalette, + tertiaryPalette, + neutralPalette, + neutralVariantPalette, + /* isDark */ false, + /* contrastLevel */ 0, + ).errorPalette; + + return { primary: primaryPalette, secondary: secondaryPalette, tertiary: tertiaryPalette, @@ -120,30 +181,36 @@ function getMapFromColorTonalPalettes( neutralVariant: neutralVariantPalette, error: errorPalette, }; - const palettes: Map> = new Map(); - for (const [key, palette] of Object.entries(tonalPalettes)) { - const paletteKey = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} + +/** + * Gets the scss representation of the provided color palettes. + * @param colorPalettes Object with all the M3 color palettes. + * @returns String of the color palettes scss. + */ +function getColorPalettesSCSS(colorPalettes: ColorPalettes): string { + let scss = '(\n'; + for (const [variant, palette] of Object.entries(colorPalettes)) { + const paletteKey = variant.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + scss += ' ' + paletteKey + ': (\n'; const tones = paletteKey === 'neutral' ? NEUTRAL_HUE_TONES : HUE_TONES; - const colorPalette: Map = new Map(); for (const tone of tones) { const color = hexFromArgb(palette.tone(tone)); - colorPalette.set(tone, color); + scss += ' ' + tone + ': ' + color + ',\n'; } - palettes.set(paletteKey, colorPalette); + scss += ' ),\n'; } - return palettes; + scss += ');'; + return scss; } /** * Gets the generated scss from the provided color palettes and theme types. - * @param colorPalettes Map of colors and their hue tones and values. + * @param colorPalettes Object with all the M3 color palettes. * @param colorComment Comment with original hex colors used to generate palettes. * @returns String of the generated theme scss. */ -export function generateSCSSTheme( - colorPalettes: Map>, - colorComment: string, -): string { +export function generateSCSSTheme(colorPalettes: ColorPalettes, colorComment: string): string { let scss = [ "// This file was generated by running 'ng generate @angular/material:theme-color'.", '// Proceed with caution if making changes to this file.', @@ -170,131 +237,85 @@ export function generateSCSSTheme( /** * Gets map of system variables and their high contrast values. - * @param primaryPalette Tonal palette that represents primary. - * @param secondaryPalette Tonal palette that represents secondary. - * @param tertiaryPalette Tonal palette that represents tertiary. - * @param neutralPalette Tonal palette that represents neutral. - * @param neutralVariantPalette Tonal palette that represents neutral variant. - * @param isDark Boolean to represent if the scheme is for a dark or light theme. + * @param colorScheme Dynamic material scheme for a high contrast theme which contains the color role values. * @returns Map of system variables names and their high contrast values. */ -function getHighContrastOverides( - primaryPalette: TonalPalette, - secondaryPalette: TonalPalette, - tertiaryPalette: TonalPalette, - neutralPalette: TonalPalette, - neutralVariantPalette: TonalPalette, - isDark: boolean, -): Map { - const scheme = getMaterialDynamicScheme( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - isDark, - 1.0, // 1.0 is the maximum contrast level - ); - +function getHighContrastOverides(colorScheme: DynamicScheme): Map { const overrides = new Map(); // Set system variables with values from primary palette - overrides.set('primary', hexFromArgb(scheme.primary)); - overrides.set('on-primary', hexFromArgb(scheme.onPrimary)); - overrides.set('primary-container', hexFromArgb(scheme.primaryContainer)); - overrides.set('on-primary-container', hexFromArgb(scheme.onPrimaryContainer)); - overrides.set('inverse-primary', hexFromArgb(scheme.inversePrimary)); - overrides.set('primary-fixed', hexFromArgb(scheme.primaryFixed)); - overrides.set('primary-fixed-dim', hexFromArgb(scheme.primaryFixedDim)); - overrides.set('on-primary-fixed', hexFromArgb(scheme.onPrimaryFixed)); - overrides.set('on-primary-fixed-variant', hexFromArgb(scheme.onPrimaryFixedVariant)); + overrides.set('primary', hexFromArgb(colorScheme.primary)); + overrides.set('on-primary', hexFromArgb(colorScheme.onPrimary)); + overrides.set('primary-container', hexFromArgb(colorScheme.primaryContainer)); + overrides.set('on-primary-container', hexFromArgb(colorScheme.onPrimaryContainer)); + overrides.set('inverse-primary', hexFromArgb(colorScheme.inversePrimary)); + overrides.set('primary-fixed', hexFromArgb(colorScheme.primaryFixed)); + overrides.set('primary-fixed-dim', hexFromArgb(colorScheme.primaryFixedDim)); + overrides.set('on-primary-fixed', hexFromArgb(colorScheme.onPrimaryFixed)); + overrides.set('on-primary-fixed-variant', hexFromArgb(colorScheme.onPrimaryFixedVariant)); // Set system variables with values from secondary palette - overrides.set('secondary', hexFromArgb(scheme.secondary)); - overrides.set('on-secondary', hexFromArgb(scheme.onSecondary)); - overrides.set('secondary-container', hexFromArgb(scheme.secondaryContainer)); - overrides.set('on-secondary-container', hexFromArgb(scheme.onSecondaryContainer)); - overrides.set('secondary-fixed', hexFromArgb(scheme.secondaryFixed)); - overrides.set('secondary-fixed-dim', hexFromArgb(scheme.secondaryFixedDim)); - overrides.set('on-secondary-fixed', hexFromArgb(scheme.onSecondaryFixed)); - overrides.set('on-secondary-fixed-variant', hexFromArgb(scheme.onSecondaryFixedVariant)); + overrides.set('secondary', hexFromArgb(colorScheme.secondary)); + overrides.set('on-secondary', hexFromArgb(colorScheme.onSecondary)); + overrides.set('secondary-container', hexFromArgb(colorScheme.secondaryContainer)); + overrides.set('on-secondary-container', hexFromArgb(colorScheme.onSecondaryContainer)); + overrides.set('secondary-fixed', hexFromArgb(colorScheme.secondaryFixed)); + overrides.set('secondary-fixed-dim', hexFromArgb(colorScheme.secondaryFixedDim)); + overrides.set('on-secondary-fixed', hexFromArgb(colorScheme.onSecondaryFixed)); + overrides.set('on-secondary-fixed-variant', hexFromArgb(colorScheme.onSecondaryFixedVariant)); // Set system variables with values from tertiary palette - overrides.set('tertiary', hexFromArgb(scheme.tertiary)); - overrides.set('on-tertiary', hexFromArgb(scheme.onTertiary)); - overrides.set('tertiary-container', hexFromArgb(scheme.tertiaryContainer)); - overrides.set('on-tertiary-container', hexFromArgb(scheme.onTertiaryContainer)); - overrides.set('tertiary-fixed', hexFromArgb(scheme.tertiaryFixed)); - overrides.set('tertiary-fixed-dim', hexFromArgb(scheme.tertiaryFixedDim)); - overrides.set('on-tertiary-fixed', hexFromArgb(scheme.onTertiaryFixed)); - overrides.set('on-tertiary-fixed-variant', hexFromArgb(scheme.onTertiaryFixedVariant)); + overrides.set('tertiary', hexFromArgb(colorScheme.tertiary)); + overrides.set('on-tertiary', hexFromArgb(colorScheme.onTertiary)); + overrides.set('tertiary-container', hexFromArgb(colorScheme.tertiaryContainer)); + overrides.set('on-tertiary-container', hexFromArgb(colorScheme.onTertiaryContainer)); + overrides.set('tertiary-fixed', hexFromArgb(colorScheme.tertiaryFixed)); + overrides.set('tertiary-fixed-dim', hexFromArgb(colorScheme.tertiaryFixedDim)); + overrides.set('on-tertiary-fixed', hexFromArgb(colorScheme.onTertiaryFixed)); + overrides.set('on-tertiary-fixed-variant', hexFromArgb(colorScheme.onTertiaryFixedVariant)); // Set system variables with values from neutral palette - overrides.set('background', hexFromArgb(scheme.background)); - overrides.set('on-background', hexFromArgb(scheme.onBackground)); - overrides.set('surface', hexFromArgb(scheme.surface)); - overrides.set('surface-dim', hexFromArgb(scheme.surfaceDim)); - overrides.set('surface-bright', hexFromArgb(scheme.surfaceBright)); - overrides.set('surface-container-lowest', hexFromArgb(scheme.surfaceContainerLowest)); - overrides.set('surface-container', hexFromArgb(scheme.surfaceContainer)); - overrides.set('surface-container-high', hexFromArgb(scheme.surfaceContainerHigh)); - overrides.set('surface-container-highest', hexFromArgb(scheme.surfaceContainerHighest)); - overrides.set('on-surface', hexFromArgb(scheme.onSurface)); - overrides.set('shadow', hexFromArgb(scheme.shadow)); - overrides.set('scrim', hexFromArgb(scheme.scrim)); - overrides.set('surface-tint', hexFromArgb(scheme.surfaceTint)); - overrides.set('inverse-surface', hexFromArgb(scheme.inverseSurface)); - overrides.set('inverse-on-surface', hexFromArgb(scheme.inverseOnSurface)); - overrides.set('outline', hexFromArgb(scheme.outline)); - overrides.set('outline-variant', hexFromArgb(scheme.outlineVariant)); + overrides.set('background', hexFromArgb(colorScheme.background)); + overrides.set('on-background', hexFromArgb(colorScheme.onBackground)); + overrides.set('surface', hexFromArgb(colorScheme.surface)); + overrides.set('surface-dim', hexFromArgb(colorScheme.surfaceDim)); + overrides.set('surface-bright', hexFromArgb(colorScheme.surfaceBright)); + overrides.set('surface-container-lowest', hexFromArgb(colorScheme.surfaceContainerLowest)); + overrides.set('surface-container', hexFromArgb(colorScheme.surfaceContainer)); + overrides.set('surface-container-high', hexFromArgb(colorScheme.surfaceContainerHigh)); + overrides.set('surface-container-highest', hexFromArgb(colorScheme.surfaceContainerHighest)); + overrides.set('on-surface', hexFromArgb(colorScheme.onSurface)); + overrides.set('shadow', hexFromArgb(colorScheme.shadow)); + overrides.set('scrim', hexFromArgb(colorScheme.scrim)); + overrides.set('surface-tint', hexFromArgb(colorScheme.surfaceTint)); + overrides.set('inverse-surface', hexFromArgb(colorScheme.inverseSurface)); + overrides.set('inverse-on-surface', hexFromArgb(colorScheme.inverseOnSurface)); + overrides.set('outline', hexFromArgb(colorScheme.outline)); + overrides.set('outline-variant', hexFromArgb(colorScheme.outlineVariant)); // Set system variables with values from error palette - overrides.set('error', hexFromArgb(scheme.error)); - overrides.set('on-error', hexFromArgb(scheme.onError)); - overrides.set('error-container', hexFromArgb(scheme.errorContainer)); - overrides.set('on-error-container', hexFromArgb(scheme.onErrorContainer)); + overrides.set('error', hexFromArgb(colorScheme.error)); + overrides.set('on-error', hexFromArgb(colorScheme.onError)); + overrides.set('error-container', hexFromArgb(colorScheme.errorContainer)); + overrides.set('on-error-container', hexFromArgb(colorScheme.onErrorContainer)); // Set system variables with values from neutral variant palette - overrides.set('surface-variant', hexFromArgb(scheme.surfaceVariant)); - overrides.set('on-surface-variant', hexFromArgb(scheme.onSurfaceVariant)); + overrides.set('surface-variant', hexFromArgb(colorScheme.surfaceVariant)); + overrides.set('on-surface-variant', hexFromArgb(colorScheme.onSurfaceVariant)); return overrides; } /** * Gets the scss representation of the high contrast override mixins. - * @param primaryPalette Tonal palette that represents primary. - * @param secondaryPalette Tonal palette that represents secondary. - * @param tertiaryPalette Tonal palette that represents tertiary. - * @param neutralPalette Tonal palette that represents neutral. - * @param neutralVariantPalette Tonal palette that represents neutral variant. + * @param lightHighContrastColorScheme Dynamic material scheme for light high contrast themes which contains the color role values. + * @param darkHighContrastColorScheme Dynamic material scheme for dark high contrast themes which contains the color role values. * @returns String of the generated high contrast mixins scss. */ function generateHighContrastOverrideMixinsSCSS( - primaryPalette: TonalPalette, - secondaryPalette: TonalPalette, - tertiaryPalette: TonalPalette, - neutralPalette: TonalPalette, - neutralVariantPalette: TonalPalette, + lightHighContrastColorScheme: DynamicScheme, + darkHighContrastColorScheme: DynamicScheme, ): string { - const lightOverrides = getHighContrastOverides( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - /** isDark **/ false, - ); - - const darkOverrides = getHighContrastOverides( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - /** isDark **/ true, - ); - // Create private function to grab correct values based on theme-type let scss = '\n'; scss += '\n@function _high-contrast-value($light, $dark, $theme-type) {\n'; @@ -311,6 +332,10 @@ function generateHighContrastOverrideMixinsSCSS( " \n @error 'Unknown theme-type #{$theme-type}. Expected light, dark, or color-scheme';\n"; scss += '}\n'; + // Populate maps with color roles values for light and dark themes to pass into mixin + const lightOverrides = getHighContrastOverides(lightHighContrastColorScheme); + const darkOverrides = getHighContrastOverides(darkHighContrastColorScheme); + // Create high contrast mixin with theme-type input that can be light, dark, or color-scheme. scss += '\n@mixin high-contrast-overrides($theme-type) {\n'; scss += ' @include mat.theme-overrides((\n'; @@ -331,119 +356,760 @@ function generateHighContrastOverrideMixinsSCSS( } /** - * Gets Hct representation of Hex color. - * @param color Hex color. - * @returns Hct color. + * Gets CSS to define a system variable with light dark values. + * @param leftSpacing Amount of spaces to add before the variable name. + * @param variableName System level variable name (ex. on-primary). + * @param lightColor Hex color for light theme. + * @param darkColor Hex color for dark theme. + * @param comment Optional comment to after the variable definition on the same line. + * @returns String with system level variable definition. */ -function getHctFromHex(color: string): Hct { - try { - return Hct.fromInt(argbFromHex(color)); - } catch (e) { - throw new Error( - 'Cannot parse the specified color ' + - color + - '. Please verify it is a hex color (ex. #ffffff or ffffff).', - ); - } +function createLightDarkVar( + leftSpacing: string, + variableName: string, + lightColor: number, + darkColor: number, + comment?: string, +) { + const commentContent = comment ? ' /* ' + comment + ' */' : ''; + const lightDarkValue = + 'light-dark(' + hexFromArgb(lightColor) + ', ' + hexFromArgb(darkColor) + ');'; + return leftSpacing + '--mat-sys-' + variableName + ': ' + lightDarkValue + commentContent + '\n'; +} + +/** + * Gets CSS for color system variables. + * @param lightScheme Dynamic material scheme for a light theme which contains the color role values. + * @param darkScheme Dynamic material scheme for a dark theme which contains the color role values. + * @param isHighContrast Boolean to represent if color variables are high contrast or not. + * @returns String with CSS for color system variables. + */ +function getColorSysVariablesCSS( + lightScheme: DynamicScheme, + darkScheme: DynamicScheme, + isHighContrast: boolean = false, +): string { + let css = ''; + + // Add extra spacing for high contrast values since variables will be nested within a media query. + let leftSpacing = ' '.repeat(isHighContrast ? 4 : 2); + + // Set system variables with values from primary palette + css += leftSpacing + '/* Primary palette variables */\n'; + // For certain color variables the value is grabbed directly from the palette rather than the role + // for the non high contrast values. The color roles keeps track of color tone relationships + // (ex. primary and primary container) and can slightly change the colors. Grabbing the value + // directly from the palette allows the colors to be consistent with how system color variables + // are defined in scss. + css += createLightDarkVar( + leftSpacing, + 'primary', + isHighContrast ? lightScheme.primary : lightScheme.primaryPalette.tone(40), + isHighContrast ? darkScheme.primary : lightScheme.primaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-primary', + isHighContrast ? lightScheme.onPrimary : lightScheme.primaryPalette.tone(100), + isHighContrast ? darkScheme.onPrimary : darkScheme.primaryPalette.tone(20), + ); + css += createLightDarkVar( + leftSpacing, + 'primary-container', + isHighContrast ? lightScheme.primaryContainer : lightScheme.primaryPalette.tone(90), + isHighContrast ? darkScheme.primaryContainer : darkScheme.primaryPalette.tone(30), + ); + css += createLightDarkVar( + leftSpacing, + 'on-primary-container', + isHighContrast ? lightScheme.onPrimaryContainer : lightScheme.primaryPalette.tone(10), + isHighContrast ? darkScheme.onPrimaryContainer : darkScheme.primaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'inverse-primary', + lightScheme.inversePrimary, + darkScheme.inversePrimary, + ); + css += createLightDarkVar( + leftSpacing, + 'primary-fixed', + isHighContrast ? lightScheme.primaryFixed : lightScheme.primaryPalette.tone(90), + isHighContrast ? darkScheme.primaryFixed : darkScheme.primaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'primary-fixed-dim', + isHighContrast ? lightScheme.primaryFixedDim : lightScheme.primaryPalette.tone(80), + isHighContrast ? darkScheme.primaryFixedDim : darkScheme.primaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-primary-fixed', + lightScheme.onPrimaryFixed, + darkScheme.onPrimaryFixed, + ); + css += createLightDarkVar( + leftSpacing, + 'on-primary-fixed-variant', + lightScheme.onPrimaryFixedVariant, + darkScheme.onPrimaryFixedVariant, + ); + + // Set system variables with values from secondary palette + css += '\n' + leftSpacing + '/* Secondary palette variables */\n'; + css += createLightDarkVar( + leftSpacing, + 'secondary', + isHighContrast ? lightScheme.secondary : lightScheme.secondaryPalette.tone(40), + isHighContrast ? darkScheme.secondary : darkScheme.secondaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-secondary', + isHighContrast ? lightScheme.onSecondary : lightScheme.secondaryPalette.tone(100), + isHighContrast ? darkScheme.onSecondary : darkScheme.secondaryPalette.tone(20), + ); + css += createLightDarkVar( + leftSpacing, + 'secondary-container', + isHighContrast ? lightScheme.secondaryContainer : lightScheme.secondaryPalette.tone(90), + isHighContrast ? darkScheme.secondaryContainer : darkScheme.secondaryPalette.tone(30), + ); + css += createLightDarkVar( + leftSpacing, + 'on-secondary-container', + isHighContrast ? lightScheme.onSecondaryContainer : lightScheme.secondaryPalette.tone(10), + isHighContrast ? darkScheme.onSecondaryContainer : darkScheme.secondaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'secondary-fixed', + isHighContrast ? lightScheme.secondaryFixed : lightScheme.secondaryPalette.tone(90), + isHighContrast ? darkScheme.secondaryFixed : darkScheme.secondaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'secondary-fixed-dim', + isHighContrast ? lightScheme.secondaryFixedDim : lightScheme.secondaryPalette.tone(80), + isHighContrast ? darkScheme.secondaryFixedDim : darkScheme.secondaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-secondary-fixed', + lightScheme.onSecondaryFixed, + darkScheme.onSecondaryFixed, + ); + css += createLightDarkVar( + leftSpacing, + 'on-secondary-fixed-variant', + lightScheme.onSecondaryFixedVariant, + darkScheme.onSecondaryFixedVariant, + ); + + // Set system variables with values from tertiary palette + css += '\n' + leftSpacing + '/* Tertiary palette variables */\n'; + css += createLightDarkVar( + leftSpacing, + 'tertiary', + isHighContrast ? lightScheme.tertiary : lightScheme.tertiaryPalette.tone(40), + isHighContrast ? darkScheme.tertiary : darkScheme.tertiaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-tertiary', + isHighContrast ? lightScheme.tertiary : lightScheme.tertiaryPalette.tone(100), + isHighContrast ? darkScheme.tertiary : darkScheme.tertiaryPalette.tone(20), + ); + css += createLightDarkVar( + leftSpacing, + 'tertiary-container', + isHighContrast ? lightScheme.tertiaryContainer : lightScheme.tertiaryPalette.tone(90), + isHighContrast ? darkScheme.tertiaryContainer : darkScheme.tertiaryPalette.tone(30), + ); + css += createLightDarkVar( + leftSpacing, + 'on-tertiary-container', + isHighContrast ? lightScheme.onTertiaryContainer : lightScheme.tertiaryPalette.tone(10), + isHighContrast ? darkScheme.onTertiaryContainer : darkScheme.tertiaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'tertiary-fixed', + isHighContrast ? lightScheme.tertiaryFixed : lightScheme.tertiaryPalette.tone(90), + isHighContrast ? darkScheme.tertiaryFixed : darkScheme.tertiaryPalette.tone(90), + ); + css += createLightDarkVar( + leftSpacing, + 'tertiary-fixed-dim', + isHighContrast ? lightScheme.tertiaryFixedDim : lightScheme.tertiaryPalette.tone(80), + isHighContrast ? darkScheme.tertiaryFixedDim : darkScheme.tertiaryPalette.tone(80), + ); + css += createLightDarkVar( + leftSpacing, + 'on-tertiary-fixed', + lightScheme.onTertiaryFixed, + darkScheme.onTertiaryFixed, + ); + css += createLightDarkVar( + leftSpacing, + 'on-tertiary-fixed-variant', + lightScheme.onTertiaryFixedVariant, + darkScheme.onTertiaryFixedVariant, + ); + + // Set system variables with values from neutral palette + css += '\n' + leftSpacing + '/* Neutral palette variables */\n'; + css += createLightDarkVar( + leftSpacing, + 'background', + lightScheme.background, + darkScheme.background, + ); + css += createLightDarkVar( + leftSpacing, + 'on-background', + lightScheme.onBackground, + darkScheme.onBackground, + ); + css += createLightDarkVar(leftSpacing, 'surface', lightScheme.surface, darkScheme.surface); + css += createLightDarkVar( + leftSpacing, + 'surface-dim', + lightScheme.surfaceDim, + darkScheme.surfaceDim, + ); + css += createLightDarkVar( + leftSpacing, + 'surface-bright', + lightScheme.surfaceBright, + darkScheme.surfaceBright, + ); + css += createLightDarkVar( + leftSpacing, + 'surface-container-lowest', + lightScheme.surfaceContainerLowest, + darkScheme.surfaceContainerLowest, + ); + css += createLightDarkVar( + leftSpacing, + 'surface-container', + lightScheme.surfaceContainer, + darkScheme.surfaceContainer, + ); + css += createLightDarkVar( + leftSpacing, + 'surface-container-high', + lightScheme.surfaceContainerHigh, + darkScheme.surfaceContainerHigh, + ); + css += createLightDarkVar( + leftSpacing, + 'surface-container-highest', + lightScheme.surfaceContainerHighest, + darkScheme.surfaceContainerHighest, + ); + css += createLightDarkVar(leftSpacing, 'on-surface', lightScheme.onSurface, darkScheme.onSurface); + css += createLightDarkVar(leftSpacing, 'shadow', lightScheme.shadow, darkScheme.shadow); + css += createLightDarkVar(leftSpacing, 'scrim', lightScheme.scrim, darkScheme.scrim); + css += createLightDarkVar( + leftSpacing, + 'surface-tint', + lightScheme.surfaceTint, + darkScheme.surfaceTint, + ); + css += createLightDarkVar( + leftSpacing, + 'inverse-surface', + lightScheme.inverseSurface, + darkScheme.inverseSurface, + ); + css += createLightDarkVar( + leftSpacing, + 'inverse-on-surface', + lightScheme.inverseOnSurface, + darkScheme.inverseOnSurface, + ); + css += createLightDarkVar(leftSpacing, 'outline', lightScheme.outline, darkScheme.outline); + css += createLightDarkVar( + leftSpacing, + 'outline-variant', + lightScheme.outlineVariant, + darkScheme.outlineVariant, + ); + css += createLightDarkVar( + leftSpacing, + 'neutral10', + lightScheme.neutralPalette.tone(10), + darkScheme.neutralPalette.tone(10), + 'Variable used for the form field native select option text color', + ); + + // Set system variables with values from error palette + css += '\n' + leftSpacing + '/* Error palette variables */\n'; + css += createLightDarkVar( + leftSpacing, + 'error', + isHighContrast ? lightScheme.error : lightScheme.errorPalette.tone(40), + isHighContrast ? darkScheme.error : darkScheme.errorPalette.tone(80), + ); + css += createLightDarkVar(leftSpacing, 'on-error', lightScheme.onError, darkScheme.onError); + css += createLightDarkVar( + leftSpacing, + 'error-container', + isHighContrast ? lightScheme.errorContainer : lightScheme.errorPalette.tone(90), + isHighContrast ? darkScheme.errorContainer : darkScheme.errorPalette.tone(30), + ); + css += createLightDarkVar( + leftSpacing, + 'on-error-container', + isHighContrast ? lightScheme.onErrorContainer : lightScheme.errorPalette.tone(10), + isHighContrast ? darkScheme.onErrorContainer : darkScheme.errorPalette.tone(90), + ); + + // Set system variables with values from neutral variant palette + css += '\n' + leftSpacing + '/* Neutral variant palette variables */\n'; + css += createLightDarkVar( + leftSpacing, + 'surface-variant', + lightScheme.surfaceVariant, + darkScheme.surfaceVariant, + ); + css += createLightDarkVar( + leftSpacing, + 'on-surface-variant', + lightScheme.onSurfaceVariant, + darkScheme.onSurfaceVariant, + ); + css += createLightDarkVar( + leftSpacing, + 'neutral-variant20', + lightScheme.neutralVariantPalette.tone(20), + darkScheme.neutralVariantPalette.tone(20), + 'Variable used for the sidenav scrim (container background shadow when opened)', + ); + + return css; +} + +/** + * Gets CSS for typography system variables. + * @returns String with CSS for typography system variables. + */ +function getTypographySysVariablesCSS(): string { + let css = ''; + + // Define typography variables to be used in the different typeface system variables + css += '\n /* Typography variables. Only used in the different typescale system variables. */\n'; + css += ' --mat-sys-brand-font-family: Roboto; /* The font-family to use for brand text. */\n'; + css += ' --mat-sys-plain-font-family: Roboto; /* The font-family to use for plain text. */\n'; + css += ' --mat-sys-bold-font-weight: 700; /* The font-weight to use for bold text. */\n'; + css += ' --mat-sys-medium-font-weight: 500; /* The font-weight to use for medium text. */\n'; + css += ' --mat-sys-regular-font-weight: 400; /* The font-weight to use for regular text. */\n\n'; + + css += ' /* Typescale variables. */\n'; + css += + ' /* Warning: Risk of reduced fidelity from using the composite typography tokens (ex. --mat-sys-body-large) since\n'; + css += + ' tracking cannot be represented in the "font" property shorthand. Consider using the discrete properties instead. */\n'; + + // Body large typescale variables + css += + ' --mat-sys-body-large: var(--mat-sys-body-large-weight) var(--mat-sys-body-large-size) / var(--mat-sys-body-large-line-height) var(--mat-sys-body-large-font);\n'; + css += ' --mat-sys-body-large-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-body-large-line-height: 1.5rem;\n'; + css += ' --mat-sys-body-large-size: 1rem;\n'; + css += ' --mat-sys-body-large-tracking: 0.031rem;\n'; + css += ' --mat-sys-body-large-weight: var(--mat-sys-regular-font-weight);\n'; + + // Body medium typescale system variables + css += '\n /* Body medium typescale */\n'; + css += + ' --mat-sys-body-medium: var(--mat-sys-body-medium-weight) var(--mat-sys-body-medium-size) / var(--mat-sys-body-medium-line-height) var(--mat-sys-body-medium-font);\n'; + css += ' --mat-sys-body-medium-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-body-medium-line-height: 1.25rem;\n'; + css += ' --mat-sys-body-medium-size: 0.875rem;\n'; + css += ' --mat-sys-body-medium-tracking: 0.016rem;\n'; + css += ' --mat-sys-body-medium-weight: var(--mat-sys-regular-font-weight);\n'; + + // Body small typescale system variables + css += '\n /* Body small typescale */\n'; + css += + ' --mat-sys-body-small: var(--mat-sys-body-small-weight) var(--mat-sys-body-small-size) / var(--mat-sys-body-small-line-height) var(--mat-sys-body-small-font);\n'; + css += ' --mat-sys-body-small-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-body-small-line-height: 1rem;\n'; + css += ' --mat-sys-body-small-size: 0.75rem;\n'; + css += ' --mat-sys-body-small-tracking: 0.025rem;\n'; + css += ' --mat-sys-body-small-weight: var(--mat-sys-regular-font-weight);\n'; + + // Display large typescale system variables + css += '\n /* Display large typescale */\n'; + css += + ' --mat-sys-display-large: var(--mat-sys-display-large-weight) var(--mat-sys-display-large-size) / var(--mat-sys-display-large-line-height) var(--mat-sys-display-large-font);\n'; + css += ' --mat-sys-display-large-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-display-large-line-height: 4rem;\n'; + css += ' --mat-sys-display-large-size: 3.562rem;\n'; + css += ' --mat-sys-display-large-tracking: -0.016rem;\n'; + css += ' --mat-sys-display-large-weight: var(--mat-sys-regular-font-weight);\n'; + + // Display medium typescale system variables + css += '\n /* Display medium typescale */\n'; + css += + ' --mat-sys-display-medium: var(--mat-sys-display-medium-weight) var(--mat-sys-display-medium-size) / var(--mat-sys-display-medium-line-height) var(--mat-sys-display-medium-font);\n'; + css += ' --mat-sys-display-medium-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-display-medium-line-height: 3.25rem;\n'; + css += ' --mat-sys-display-medium-size: 2.812rem;\n'; + css += ' --mat-sys-display-medium-tracking: 0;\n'; + css += ' --mat-sys-display-medium-weight: var(--mat-sys-regular-font-weight);\n'; + + // Display small typescale system variables + css += '\n /* Display small typescale */\n'; + css += + ' --mat-sys-display-small: var(--mat-sys-display-small-weight) var(--mat-sys-display-small-size) / var(--mat-sys-display-small-line-height) var(--mat-sys-display-small-font);\n'; + css += ' --mat-sys-display-small-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-display-small-line-height: 2.75rem;\n'; + css += ' --mat-sys-display-small-size: 2.25rem;\n'; + css += ' --mat-sys-display-small-tracking: 0;\n'; + css += ' --mat-sys-display-small-weight: var(--mat-sys-regular-font-weight);\n'; + + // Headline large typescale system variables + css += '\n /* Headline large typescale */\n'; + css += + ' --mat-sys-headline-large: var(--mat-sys-headline-large-weight) var(--mat-sys-headline-large-size) / var(--mat-sys-headline-large-line-height) var(--mat-sys-headline-large-font);\n'; + css += ' --mat-sys-headline-large-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-headline-large-line-height: 2.5rem;\n'; + css += ' --mat-sys-headline-large-size: 2rem;\n'; + css += ' --mat-sys-headline-large-tracking: 0;\n'; + css += ' --mat-sys-headline-large-weight: var(--mat-sys-regular-font-weight);\n'; + + // Headline medium typescale system variables + css += '\n /* Headline medium typescale */\n'; + css += + ' --mat-sys-headline-medium: var(--mat-sys-headline-medium-weight) var(--mat-sys-headline-medium-size) / var(--mat-sys-headline-medium-line-height) var(--mat-sys-headline-medium-font);\n'; + css += ' --mat-sys-headline-medium-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-headline-medium-line-height: 2.25rem;\n'; + css += ' --mat-sys-headline-medium-size: 1.75rem;\n'; + css += ' --mat-sys-headline-medium-tracking: 0;\n'; + css += ' --mat-sys-headline-medium-weight: var(--mat-sys-regular-font-weight);\n'; + + // Headline small typescale system variables + css += '\n /* Headline small typescale */\n'; + css += + ' --mat-sys-headline-small: var(--mat-sys-headline-small-weight) var(--mat-sys-headline-small-size) / var(--mat-sys-headline-small-line-height) var(--mat-sys-headline-small-font);\n'; + css += ' --mat-sys-headline-small-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-headline-small-line-height: 2rem;\n'; + css += ' --mat-sys-headline-small-size: 1.5rem;\n'; + css += ' --mat-sys-headline-small-tracking: 0;\n'; + css += ' --mat-sys-headline-small-weight: var(--mat-sys-regular-font-weight);\n'; + + // Label large typescale system variables + css += '\n /* Label large typescale */\n'; + css += + ' --mat-sys-label-large: var(--mat-sys-label-large-weight) var(--mat-sys-label-large-size) / var(--mat-sys-label-large-line-height) var(--mat-sys-label-large-font);\n'; + css += ' --mat-sys-label-large-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-label-large-line-height: 1.25rem;\n'; + css += ' --mat-sys-label-large-size: 0.875rem;\n'; + css += ' --mat-sys-label-large-tracking: 0.006rem;\n'; + css += ' --mat-sys-label-large-weight: var(--mat-sys-medium-font-weight);\n'; + css += ' --mat-sys-label-large-weight-prominent: var(--mat-sys-bold-font-weight);\n'; + + // Label medium typescale system variables + css += '\n /* Label medium typescale */\n'; + css += + ' --mat-sys-label-medium: var(--mat-sys-label-medium-weight) var(--mat-sys-label-medium-size) / var(--mat-sys-label-medium-line-height) var(--mat-sys-label-medium-font);\n'; + css += ' --mat-sys-label-medium-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-label-medium-line-height: 1rem;\n'; + css += ' --mat-sys-label-medium-size: 0.75rem;\n'; + css += ' --mat-sys-label-medium-tracking: 0.031rem;\n'; + css += ' --mat-sys-label-medium-weight: var(--mat-sys-medium-font-weight);\n'; + css += ' --mat-sys-label-medium-weight-prominent: var(--mat-sys-bold-font-weight);\n'; + + // Label small typescale system variables + css += '\n /* Label small typescale */\n'; + css += + ' --mat-sys-label-small: var(--mat-sys-label-small-weight) var(--mat-sys-label-small-size) / var(--mat-sys-label-small-line-height) var(--mat-sys-label-small-font);\n'; + css += ' --mat-sys-label-small-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-label-small-line-height: 1rem;\n'; + css += ' --mat-sys-label-small-size: 0.688rem;\n'; + css += ' --mat-sys-label-small-tracking: 0.031rem;\n'; + css += ' --mat-sys-label-small-weight: var(--mat-sys-medium-font-weight);\n'; + + // Title large typescale system variables + css += '\n /* Title large typescale */\n'; + css += + ' --mat-sys-title-large: var(--mat-sys-title-large-weight) var(--mat-sys-title-large-size) / var(--mat-sys-title-large-line-height) var(--mat-sys-title-large-font);\n'; + css += ' --mat-sys-title-large-font: var(--mat-sys-brand-font-family);\n'; + css += ' --mat-sys-title-large-line-height: 1.75rem;\n'; + css += ' --mat-sys-title-large-size: 1.375rem;\n'; + css += ' --mat-sys-title-large-tracking: 0;\n'; + css += ' --mat-sys-title-large-weight: var(--mat-sys-regular-font-weight);\n'; + + // Title medium typescale system variables + css += '\n /* Title medium typescale */\n'; + css += + ' --mat-sys-title-medium: var(--mat-sys-title-medium-weight) var(--mat-sys-title-medium-size) / var(--mat-sys-title-medium-line-height) var(--mat-sys-title-medium-font);\n'; + css += ' --mat-sys-title-medium-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-title-medium-line-height: 1.5rem;\n'; + css += ' --mat-sys-title-medium-size: 1rem;\n'; + css += ' --mat-sys-title-medium-tracking: 0.009rem;\n'; + css += ' --mat-sys-title-medium-weight: var(--mat-sys-medium-font-weight);\n'; + + // Title small typescale system variables + css += '\n /* Title small typescale */\n'; + css += + ' --mat-sys-title-small: var(--mat-sys-title-small-weight) var(--mat-sys-title-small-size) / var(--mat-sys-title-small-line-height) var(--mat-sys-title-small-font);\n'; + css += ' --mat-sys-title-small-font: var(--mat-sys-plain-font-family);\n'; + css += ' --mat-sys-title-small-line-height: 1.25rem;\n'; + css += ' --mat-sys-title-small-size: 0.875rem;\n'; + css += ' --mat-sys-title-small-tracking: 0.006rem;\n'; + css += ' --mat-sys-title-small-weight: var(--mat-sys-medium-font-weight);\n'; + + return css; +} + +/** + * Gets CSS for elevation system variables. + * @returns String with CSS for elevation system variables. + */ +function getElevationSysVariablesCSS(): string { + let css = ''; + + css += '\n /* Box shadow colors. Only used in the elevation level system variables. */\n'; + css += ' --mat-sys-umbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 80%);\n'; + css += + ' --mat-sys-penumbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 86%);\n'; + css += ' --mat-sys-ambient-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 88%);\n'; + + css += + '\n /* Elevation level system variables. These are used as the value for box-shadow CSS property. */\n'; + css += + ' --mat-sys-level0: 0px 0px 0px 0px var(--mat-sys-umbra-color), 0px 0px 0px 0px var(--mat-sys-penumbra-color), 0px 0px 0px 0px var(--mat-sys-ambient-color);\n'; + css += + ' --mat-sys-level1: 0px 2px 1px -1px var(--mat-sys-umbra-color), 0px 1px 1px 0px var(--mat-sys-penumbra-color), 0px 1px 3px 0px var(--mat-sys-ambient-color);\n'; + css += + ' --mat-sys-level2: 0px 3px 3px -2px var(--mat-sys-umbra-color), 0px 3px 4px 0px var(--mat-sys-penumbra-color), 0px 1px 8px 0px var(--mat-sys-ambient-color);\n'; + css += + ' --mat-sys-level3: 0px 3px 5px -1px var(--mat-sys-umbra-color), 0px 6px 10px 0px var(--mat-sys-penumbra-color), 0px 1px 18px 0px var(--mat-sys-ambient-color);\n'; + css += + ' --mat-sys-level4: 0px 5px 5px -3px var(--mat-sys-umbra-color), 0px 8px 10px 1px var(--mat-sys-penumbra-color), 0px 3px 14px 2px var(--mat-sys-ambient-color);\n'; + css += + ' --mat-sys-level5: 0px 7px 8px -4px var(--mat-sys-umbra-color), 0px 12px 17px 2px var(--mat-sys-penumbra-color), 0px 5px 22px 4px var(--mat-sys-ambient-color);\n'; + + return css; +} + +/** + * Gets CSS for shape system variables. + * @returns String with CSS for shape system variables. + */ +function getShapeSysVariablesCSS(): string { + let css = ''; + css += ' --mat-sys-corner-extra-large: 28px;\n'; + css += ' --mat-sys-corner-extra-large-top: 28px 28px 0 0;\n'; + css += ' --mat-sys-corner-extra-small: 4px;\n'; + css += ' --mat-sys-corner-extra-small-top: 4px 4px 0 0;\n'; + css += ' --mat-sys-corner-full: 9999px;\n'; + css += ' --mat-sys-corner-large: 16px;\n'; + css += ' --mat-sys-corner-large-end: 0 16px 16px 0;\n'; + css += ' --mat-sys-corner-large-start: 16px 0 0 16px;\n'; + css += ' --mat-sys-corner-large-top: 16px 16px 0 0;\n'; + css += ' --mat-sys-corner-medium: 12px;\n'; + css += ' --mat-sys-corner-none: 0;\n'; + css += ' --mat-sys-corner-small: 8px;\n'; + return css; +} + +/** + * Gets CSS for state system variables. + * @returns String with CSS for state system variables. + */ +function getStateSysVariablesCSS(): string { + let css = ''; + css += ' --mat-sys-dragged-state-layer-opacity: 0.16;\n'; + css += ' --mat-sys-focus-state-layer-opacity: 0.12;\n'; + css += ' --mat-sys-hover-state-layer-opacity: 0.08;\n'; + css += ' --mat-sys-pressed-state-layer-opacity: 0.12;\n'; + return css; +} + +/** + * Gets CSS for all system variables. + * @param lightColorScheme Dynamic material scheme for a light high contrast theme which contains the color role values. + * @param darkColorScheme Dynamic material scheme for a dark high contrast theme which contains the color role values. + * @returns String with CSS for all system variables. + */ +export function getAllSysVariablesCSS( + lightColorScheme: DynamicScheme, + darkColorScheme: DynamicScheme, +): string { + let css = ''; + + css += ' /* COLOR SYSTEM VARIABLES */\n'; + css += ' color-scheme: light;\n\n'; + css += getColorSysVariablesCSS(lightColorScheme, darkColorScheme); + + css += '\n /* TYPOGRAPHY SYSTEM VARIABLES */\n'; + css += getTypographySysVariablesCSS(); + + css += '\n /* ELEVATION SYSTEM VARIABLES */\n'; + css += getElevationSysVariablesCSS(); + + css += '\n /* SHAPE SYSTEM VARIABLES */\n'; + css += getShapeSysVariablesCSS(); + + css += '\n /* STATE SYSTEM VARIABLES */\n'; + css += getStateSysVariablesCSS(); + + return css; +} + +/** + * Gets CSS for high contrast color values to be automatically used when users specify. + * @param lightColorScheme Dynamic material scheme for a light high contrast theme which contains the color role values. + * @param darkColorScheme Dynamic material scheme for a dark high contrast theme which contains the color role values. + * @returns String with CSS for high contrast media query and values. + */ +export function getHighContrastOverridesCSS( + lightColorScheme: DynamicScheme, + darkColorScheme: DynamicScheme, +): string { + let css = '\n'; + css += ' @media (prefers-contrast: more) {\n'; + css += getColorSysVariablesCSS(lightColorScheme, darkColorScheme, /* isHighContrast */ true); + css += ' }\n'; + return css; } /** * Creates theme file for provided scss. - * @param scss scss for the theme file. + * @param content String content for the theme file. * @param tree Directory tree. + * @param directory Path for generated file. + * @param isScss Whether or not the generated file is a scss file. If false will output a CSS file. * @param directory Directory path to place generated theme file. */ -function createThemeFile(scss: string, tree: Tree, directory?: string) { - const filePath = directory ? directory + '_theme-colors.scss' : '_theme-colors.scss'; - tree.create(filePath, scss); +function createThemeFile(content: string, tree: Tree, directory?: string, isScss: boolean = true) { + const fileName = isScss ? '_theme-colors.scss' : 'theme.css'; + const filePath = directory ? directory + fileName : fileName; + tree.create(filePath, content); +} + +/** + * Gets the comment that lists the color customizations specified from the schematic inputs. + * @param primaryColor Hex color that represents the primary palette. + * @param secondaryColor Hex color that represents the secondary palette. + * @param tertiaryColor Hex color that represents the tertiary palette. + * @param neutralColor Hex color that represents the neutral palette. + * @returns String with specified color customizations + */ +function getColorComment( + primaryColor: string, + secondaryColor?: string, + tertiaryColor?: string, + neutralColor?: string, +) { + let colorComment = 'Color palettes are generated from primary: ' + primaryColor; + if (secondaryColor) { + colorComment += ', secondary: ' + secondaryColor; + } + if (tertiaryColor) { + colorComment += ', tertiary: ' + tertiaryColor; + } + if (neutralColor) { + colorComment += ', neutral: ' + neutralColor; + } + return colorComment; } export default function (options: Schema): Rule { return async (tree: Tree, context: SchematicContext) => { - let colorComment = 'Color palettes are generated from primary: ' + options.primaryColor; - - // Create tonal palettes for each color and custom color overrides if applicable. Used for both - // standard contrast and high contrast schemes since they share the same tonal palettes. - // The math to generate the palettes follows how palettes are generated for SchemeFidelity - // (https://github.com/material-foundation/material-color-utilities/blob/main/typescript/scheme/scheme_fidelity.ts). - // Cannot create object directly since we allow users to enter custom colors for palettes and - // palettes are readonly for a DynamicScheme. - const primaryColorHct = getHctFromHex(options.primaryColor); - const primaryPalette = TonalPalette.fromHct(primaryColorHct); - - let secondaryPalette; - if (options.secondaryColor) { - colorComment += ', secondary: ' + options.secondaryColor; - secondaryPalette = TonalPalette.fromHct(getHctFromHex(options.secondaryColor)); - } else { - secondaryPalette = TonalPalette.fromHueAndChroma( - primaryColorHct.hue, - Math.max(primaryColorHct.chroma - 32.0, primaryColorHct.chroma * 0.5), + const colorComment = getColorComment( + options.primaryColor, + options.secondaryColor, + options.tertiaryColor, + options.neutralColor, + ); + + const colorPalettes = getColorPalettes( + options.primaryColor, + options.secondaryColor, + options.tertiaryColor, + options.neutralColor, + ); + + let lightHighContrastColorScheme: DynamicScheme; + let darkHighContrastColorScheme: DynamicScheme; + if (options.includeHighContrast) { + lightHighContrastColorScheme = getMaterialDynamicScheme( + colorPalettes.primary, + colorPalettes.secondary, + colorPalettes.tertiary, + colorPalettes.neutral, + colorPalettes.neutralVariant, + /* isDark */ false, + /* contrastLevel */ 1.0, ); - } - let tertiaryPalette; - if (options.tertiaryColor) { - colorComment += ', tertiary: ' + options.tertiaryColor; - tertiaryPalette = TonalPalette.fromHct(getHctFromHex(options.tertiaryColor)); - } else { - tertiaryPalette = TonalPalette.fromInt( - DislikeAnalyzer.fixIfDisliked( - new TemperatureCache(primaryColorHct).analogous(3, 6)[2], - ).toInt(), + darkHighContrastColorScheme = getMaterialDynamicScheme( + colorPalettes.primary, + colorPalettes.secondary, + colorPalettes.tertiary, + colorPalettes.neutral, + colorPalettes.neutralVariant, + /* isDark */ true, + /* contrastLevel */ 1.0, ); } - let neutralPalette; - if (options.neutralColor) { - colorComment += ', neutral: ' + options.neutralColor; - neutralPalette = TonalPalette.fromHct(getHctFromHex(options.neutralColor)); + if (options.isScss) { + let themeScss = generateSCSSTheme(colorPalettes, colorComment); + + // Add high contrast overrides mixins to generated file if specified + if (options.includeHighContrast) { + themeScss += generateHighContrastOverrideMixinsSCSS( + lightHighContrastColorScheme!, + darkHighContrastColorScheme!, + ); + } + + createThemeFile(themeScss, tree, options.directory); } else { - neutralPalette = TonalPalette.fromHueAndChroma( - primaryColorHct.hue, - primaryColorHct.chroma / 8.0, + let themeCss = ''; + themeCss += '/* Note: ' + colorComment + ' */\n'; + themeCss += 'html {\n'; + + const lightColorScheme = getMaterialDynamicScheme( + colorPalettes.primary, + colorPalettes.secondary, + colorPalettes.tertiary, + colorPalettes.neutral, + colorPalettes.neutralVariant, + /* isDark */ false, + /* contrastLevel */ 0, ); - } - const neutralVariantPalette = TonalPalette.fromHueAndChroma( - primaryColorHct.hue, - primaryColorHct.chroma / 8.0 + 4.0, - ); + const darkColorScheme = getMaterialDynamicScheme( + colorPalettes.primary, + colorPalettes.secondary, + colorPalettes.tertiary, + colorPalettes.neutral, + colorPalettes.neutralVariant, + /* isDark */ true, + /* contrastLevel */ 0, + ); - // Create material dynamic scheme to generate the error tonal palette - const errorPalette = getMaterialDynamicScheme( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - false, - 0, - ).errorPalette; - - // Create the generated SCSS file with the exportable palette users can use in their `theme` - // mixin call. - const colorPalettes = getMapFromColorTonalPalettes( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - errorPalette, - ); - let themeScss = generateSCSSTheme(colorPalettes, colorComment); + themeCss += getAllSysVariablesCSS(lightColorScheme, darkColorScheme); - // Add high contrast overrides mixins to generated file if specified - if (options.includeHighContrast) { - themeScss += generateHighContrastOverrideMixinsSCSS( - primaryPalette, - secondaryPalette, - tertiaryPalette, - neutralPalette, - neutralVariantPalette, - ); - } + // Add high contrast media query to overwrite the color values when the user specifies + if (options.includeHighContrast) { + themeCss += getHighContrastOverridesCSS( + lightHighContrastColorScheme!, + darkHighContrastColorScheme!, + ); + } - createThemeFile(themeScss, tree, options.directory); + themeCss += '}\n'; + createThemeFile(themeCss, tree, options.directory, /* isScss */ false); + } }; } diff --git a/src/material/schematics/ng-generate/theme-color/schema.d.ts b/src/material/schematics/ng-generate/theme-color/schema.d.ts index 88a738475a70..8e26e2e0ab01 100644 --- a/src/material/schematics/ng-generate/theme-color/schema.d.ts +++ b/src/material/schematics/ng-generate/theme-color/schema.d.ts @@ -34,4 +34,12 @@ export interface Schema { * If not set, the file will be generated at the project root. */ directory?: string; + + /** + * Whether to generate output file in scss or CSS. CSS directly defines all the system variables + * instead of having a separate theme scss file where you call the `theme()` mixin. + * + * If not set, the generated theme file will be a scss file. + */ + isScss?: boolean; } diff --git a/src/material/schematics/ng-generate/theme-color/schema.json b/src/material/schematics/ng-generate/theme-color/schema.json index 9369044d2219..272f6e47dc27 100644 --- a/src/material/schematics/ng-generate/theme-color/schema.json +++ b/src/material/schematics/ng-generate/theme-color/schema.json @@ -34,6 +34,12 @@ "type": "string", "description": "Workspace-relative path to a directory where generated theme file will be created", "x-prompt": "What is the directory you want to place the generated theme file in? (Enter the relative path such as 'src/app/styles/' or leave blank to generate at your project root)" + }, + "isScss": { + "type": "boolean", + "default": true, + "description": "Whether to generate output file in scss or CSS", + "x-prompt": "Do you want to generated file to be a scss file? This is the recommended way of setting up theming in your application. If not, a CSS file will be generated with all the system variables defined. (Leave blank to generate a scss file)" } } }