diff --git a/src/app/color_theme_loader.scss b/src/app/color_theme_loader.scss new file mode 100644 index 0000000..fe99100 --- /dev/null +++ b/src/app/color_theme_loader.scss @@ -0,0 +1,3 @@ +@use 'colors'; + +@include colors.workflow-graph-colors(); diff --git a/src/app/color_theme_loader.spec.ts b/src/app/color_theme_loader.spec.ts new file mode 100644 index 0000000..e1d1157 --- /dev/null +++ b/src/app/color_theme_loader.spec.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {DOCUMENT} from '@angular/common'; +import {Component, ViewChild} from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, waitForAsync} from '@angular/core/testing'; +import {BehaviorSubject} from 'rxjs'; + +import {ColorThemeLoader} from './color_theme_loader'; +import {DagStateService} from './dag-state.service'; +import {defaultFeatures, FeatureToggleOptions} from './data_types_internal'; +import {initTestBed, keyWithCtrlOrCommand} from './test_resources/test_utils'; + + +describe('ColorThemeLoader', () => { + let toggleSpy: jasmine.Spy; + let loaderInstance: ColorThemeLoader; + const fakeFeatures = + new BehaviorSubject(defaultFeatures); + + beforeEach(waitForAsync(async () => { + fakeFeatures.next(defaultFeatures); + + const fakeDagStateService = + jasmine.createSpyObj('DagStateService', ['features$']); + fakeDagStateService.features$ = fakeFeatures; + + await initTestBed({ + declarations: [TestComponent], + imports: [ColorThemeLoader], + providers: [ + { + provide: DagStateService, + useValue: fakeDagStateService, + }, + ], + }); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + loaderInstance = fixture.componentInstance.loader; + toggleSpy = spyOn( + fixture.debugElement.injector.get(DOCUMENT).body.classList, 'toggle'); + })); + + it('should set light mode by default', async () => { + expect( + window.document.body.classList.contains('workflow-graph-theme-light')) + .toBeTrue(); + expect(window.document.body.classList.contains('workflow-graph-theme-dark')) + .toBeFalse(); + }) + + describe('when dark mode is set in features', () => { + beforeEach(waitForAsync(async () => { + fakeFeatures.next({ + ...defaultFeatures, + theme: 'dark', + }); + })); + + it('should set dark mode', async () => { + expect(toggleSpy).toHaveBeenCalledWith( + 'workflow-graph-theme-light', false); + expect(toggleSpy).toHaveBeenCalledWith('workflow-graph-theme-dark', true); + }) + }) + + describe('when the theme is set to device', () => { + beforeEach(waitForAsync(async () => { + fakeFeatures.next({ + ...defaultFeatures, + theme: 'device', + }); + })); + + describe('when the user prefers dark mode', () => { + beforeEach(waitForAsync(async () => { + toggleSpy.calls.reset(); + loaderInstance.prefersColorSchemeDarkMediaQuery.dispatchEvent( + new MediaQueryListEvent('change', { + matches: true, + media: '(prefers-color-scheme: dark)', + })); + })); + + it('should set dark mode', () => { + expect(toggleSpy).toHaveBeenCalledWith( + 'workflow-graph-theme-light', false); + expect(toggleSpy).toHaveBeenCalledWith( + 'workflow-graph-theme-dark', true); + }); + }); + }); +}); + +@Component({ + standalone: false, + template: + '', + jit: true, +}) +class TestComponent { + @ViewChild('loader', {static: false}) loader!: ColorThemeLoader; +} \ No newline at end of file diff --git a/src/app/color_theme_loader.ts b/src/app/color_theme_loader.ts new file mode 100644 index 0000000..e1506de --- /dev/null +++ b/src/app/color_theme_loader.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {DOCUMENT} from '@angular/common'; +import {Component, DestroyRef, Inject, ViewEncapsulation} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {combineLatest, fromEvent, Observable} from 'rxjs'; +import {distinctUntilChanged, map, startWith} from 'rxjs/operators'; + +import {DagStateService} from './dag-state.service'; +import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; +import {Theme} from './data_types_internal'; + +@Component({ + selector: 'workflow-graph-color-theme-loader', + template: '', + styleUrls: ['./color_theme_loader.scss'], + standalone: true, + encapsulation: ViewEncapsulation.None, + providers: [ + STATE_SERVICE_PROVIDER, + ], +}) +export class ColorThemeLoader { + private readonly theme: Observable = + this.dagStateService.features$.pipe( + map(features => features.theme), distinctUntilChanged()); + readonly prefersColorSchemeDarkMediaQuery: MediaQueryList = window.matchMedia( + '(prefers-color-scheme: dark)', + ); + private readonly deviceColorSchemeDarkChange: Observable = + fromEvent( + this.prefersColorSchemeDarkMediaQuery, + 'change', + ) + .pipe( + map((event) => event.matches), + startWith(this.prefersColorSchemeDarkMediaQuery.matches), + ); + + constructor( + private readonly dagStateService: DagStateService, + @Inject(DOCUMENT) private readonly document: Document, + private readonly destroyRef: DestroyRef) { + combineLatest([this.theme, this.deviceColorSchemeDarkChange]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([theme, isDeviceColorDark]) => { + const isDarkTheme = + theme === 'dark' || (theme === 'device' && isDeviceColorDark); + this.document.body.classList.toggle( + 'workflow-graph-theme-dark', isDarkTheme); + this.document.body.classList.toggle( + 'workflow-graph-theme-light', !isDarkTheme); + }); + } +} \ No newline at end of file diff --git a/src/app/colors.scss b/src/app/colors.scss new file mode 100644 index 0000000..9356514 --- /dev/null +++ b/src/app/colors.scss @@ -0,0 +1,199 @@ +@mixin colors() { + --workflow-graph-color-primary-container: var( + --cm-sys-color-primary-container, + #e8f0fe + ); + --workflow-graph-color-surface: var(--cm-sys-color-surface, #fff); + --workflow-graph-color-surface-variant: var( + --cm-sys-color-surface-variant, + #f9fafb + ); + --workflow-graph-color-surface-on-surface: var( + --cm-sys-color-on-surface, + #202124 + ); + --workflow-graph-color-surface-on-surface-variant: var( + --cm-sys-color-on-surface-variant, + #5f6368 + ); + --workflow-graph-color-surface-elevation-1: var( + --cm-sys-color-surface-elevation1, + #fff + ); + --workflow-graph-color-hairline: var(--cm-sys-color-hairline, #dadce0); + --workflow-graph-color-non-primary: var(--cm-sys-color-non-primary, #5f6368); + --workflow-graph-color-outline: #263c4043; + --workflow-graph-color-neutral-container: var( + --cm-sys-color-neutral-container, + #f1f3f4 + ); + --workflow-graph-color-on-primary: var(--cm-sys-color-on-primary, #fff); + --workflow-graph-color-status-error: var( + --cm-sys-color-status-error, + #d93025 + ); + --workflow-graph-color-status-on-error: var( + --cm-sys-color-status-on-error, + #fff + ); + --workflow-graph-color-state-disabled: var( + --cm-sys-color-state-disabled, + #b5b6b8 + ); + --workflow-graph-color-state-pending: #9ba1a7; + --workflow-graph-color-state-disabled-container: var( + --cm-sys-color-state-disabled-container, + #e8e8e8 + ); + + --workflow-graph-base-color-black: #171717; + --workflow-graph-base-color-blue: #1a73e8; + --workflow-graph-base-color-blue-transparent-05: transparentize(#1a73e8, 0.5); + --workflow-graph-base-color-blue-transparent-088: transparentize( + #1a73e8, + 0.88 + ); + --workflow-graph-base-color-blue-transparent-095: transparentize( + #1a73e8, + 0.95 + ); + --workflow-graph-base-color-blue-transparent-1: transparentize(#1a73e8, 1); + --workflow-graph-base-color-green: var( + --cm-sys-color-status-success, + #188038 + ); + --workflow-graph-base-color-red: var(--cm-sys-color-status-error, #d93025); + --workflow-graph-base-color-yellow: var( + --cm-sys-color-status-warning, + #e37400 + ); + --workflow-graph-base-color-orange: #fbbc04; + --workflow-graph-base-color-purple: #af5cf7; + --workflow-graph-base-color-gray: #5f6368; + + --workflow-graph-base-color-background-blue: #e8f0fe; + --workflow-graph-base-color-background-green: var( + --cm-sys-color-status-success-container, + #e6f4ea + ); + --workflow-graph-base-color-background-red: var( + --cm-sys-color-status-error-container, + #fce8e6 + ); + --workflow-graph-base-color-background-yellow: var( + --cm-sys-color-status-warning-container, + #fef7e0 + ); + --workflow-graph-base-color-background-gray: var( + --cm-sys-color-status-neutral-container, + #f0f0f0 + ); + --workflow-graph-base-color-background-white: #fff; +} + +@mixin colors-dark() { + --workflow-graph-color-primary-container: var( + --cm-sys-color-primary-container, + #394457 + ); + --workflow-graph-color-surface: var(--cm-sys-color-surface, #131314); + --workflow-graph-color-surface-variant: var( + --cm-sys-color-surface-variant, + #1f2123 + ); + --workflow-graph-color-hairline: var(--cm-sys-color-hairline, #5f6368); + --workflow-graph-color-non-primary: var(--cm-sys-color-non-primary, #e8eaed); + --workflow-graph-color-outline: var(--cm-sys-color-outline, #9aa0a6); + --workflow-graph-color-neutral-container: var( + --cm-sys-color-neutral-container, + #4d4e51 + ); + --workflow-graph-color-on-primary: var(--cm-sys-color-on-primary, #202124); + --workflow-graph-color-status-error: var( + --cm-sys-color-status-error, + #ee675c + ); + --workflow-graph-color-status-on-error: var( + --cm-sys-color-status-on-error, + #000 + ); + --workflow-graph-color-state-disabled: var( + --cm-sys-color-state-disabled, + #6c6d70 + ) + --workflow-graph-color-state-disabled-container: var( + --cm-sys-color-state-disabled-container, + #38393c + ); + + --workflow-graph-color-surface-on-surface: var( + --cm-sys-color-on-surface, + #e8eaed + ); + --workflow-graph-color-surface-on-surface-variant: var( + --cm-sys-color-on-surface-variant, + #9aa0a6 + ); + --workflow-graph-color-surface-elevation-1: var( + --cm-sys-color-surface-elevation1, + #2a2b2e + ); + + --workflow-graph-base-color-black: #171717; + --workflow-graph-base-color-blue: #8ab4f8; + --workflow-graph-base-color-blue-transparent-05: transparentize(#8ab4f8, 0.5); + --workflow-graph-base-color-blue-transparent-088: transparentize( + #8ab4f8, + 0.88 + ); + --workflow-graph-base-color-blue-transparent-095: transparentize( + #8ab4f8, + 0.95 + ); + --workflow-graph-base-color-blue-transparent-1: transparentize(#8ab4f8, 1); + --workflow-graph-base-color-green: var( + --cm-sys-color-status-success, + #5bb974 + ); + --workflow-graph-base-color-red: var(--cm-sys-color-status-error, #ee675c); + --workflow-graph-base-color-yellow: var( + --cm-sys-color-status-warning, + #fcc934 + ); + --workflow-graph-base-color-orange: #fbbc04; + --workflow-graph-base-color-purple: #af5cf7; + --workflow-graph-base-color-gray: #5f6368; + + --workflow-graph-base-color-background-blue: #020a17; + --workflow-graph-base-color-background-green: var( + --cm-sys-color-status-success-container, + #37493f + ); + --workflow-graph-base-color-background-red: var( + --cm-sys-color-status-error-container, + #523a3b + ); + --workflow-graph-base-color-background-yellow: var( + --cm-sys-color-status-warning-container, + #554c33 + ); + --workflow-graph-base-color-background-gray: var( + --cm-sys-color-status-neutral-container, + #1e1e1f + ); + --workflow-graph-base-color-background-white: #fff; +} + +@mixin workflow-graph-colors($parentSelector: 'body') { + @layer styles; + + @layer styles { + #{$parentSelector} { + @include colors(); + } + + #{$parentSelector}.workflow-graph-theme-dark { + @include colors-dark(); + } + } +} diff --git a/src/app/data_types_internal.ts b/src/app/data_types_internal.ts index 77bbafd..c9a83bd 100644 --- a/src/app/data_types_internal.ts +++ b/src/app/data_types_internal.ts @@ -409,6 +409,9 @@ export function createDefaultZoomConfig(opts: Partial): ZoomConfig { return Object.assign({}, defaultZoomConfig, opts); } +/** The color theme to use for DAG color variables */ +export type Theme = 'device'|'light'|'dark'; + /** * The different features that can be toggled on the DAG Component */ @@ -475,6 +478,12 @@ export interface FeatureToggleOptions { * Defaults to `false`. */ disableLoadingMaterialStyles?: boolean; + + /** + * Specifies the workflow graph color theme to use. Defaults to `light`. + * Custom themes can override the theme behavior. + */ + theme?: Theme; } /** * Default set of functionality to enable / disable in the DAG Component @@ -497,6 +506,7 @@ export const defaultFeatures: FeatureToggleOptions = { naturalScrolling: false, hideProgressCell: false, disableLoadingMaterialStyles: false, + theme: 'light', }; /** @@ -610,30 +620,29 @@ export function generateTheme(theme: PartialDagTheme) { * Component */ export const baseColors = { - 'black': '#171717', - 'blue': '#1A73E8', - 'green': '#1E8E3E', - 'red': '#E94235', - 'yellow': '#D56E03', - 'orange': '#fbbc04', - 'purple': '#af5cf7', - 'gray': '#5F6368', + 'black': 'var(--workflow-graph-base-color-black)', + 'blue': 'var(--workflow-graph-base-color-blue)', + 'green': 'var(--workflow-graph-base-color-green)', + 'red': 'var(--workflow-graph-base-color-red)', + 'yellow': 'var(--workflow-graph-base-color-yellow)', + 'orange': 'var(--workflow-graph-base-color-orange)', + 'purple': 'var(--workflow-graph-base-color-purple)', + 'gray': 'var(--workflow-graph-base-color-gray)', 'bg': { - 'blue': '#E8F0FE', - 'green': '#E6F4EA', - 'red': '#FCE8E6', - 'yellow': '#FEF7E0', - 'gray': '#f0f0f0', - 'white': '#FFFFFF', + 'blue': 'var(--workflow-graph-base-color-background-blue)', + 'green': 'var(--workflow-graph-base-color-background-green)', + 'red': 'var(--workflow-graph-base-color-background-red)', + 'yellow': 'var(--workflow-graph-base-color-background-yellow)', + 'gray': 'var(--workflow-graph-base-color-background-gray)', + 'white': 'var(--workflow-graph-base-color-background-white)', 'none': 'transparent', 'dots': { - 'gray': '#e0e0e0', - 'darkGray': '#cccccc', + 'gray': 'var(--workflow-graph-color-hairline)', } }, 'minimap': { - 'gray': '#DFDFDF', - 'blue': '#4285F4', + 'gray': 'var(--workflow-graph-base-color-gray)', + 'blue': 'var(--workflow-graph-base-color-blue)', } }; diff --git a/src/app/demo_page/demo_page.ng.html b/src/app/demo_page/demo_page.ng.html index 14f3d8d..626727d 100644 --- a/src/app/demo_page/demo_page.ng.html +++ b/src/app/demo_page/demo_page.ng.html @@ -101,11 +101,12 @@ rgba(0, 0, 0, 0.005), rgba(0, 0, 0, 0.005) 5px, rgba(0, 0, 0, 0.05) 5px, - rgba(0, 0, 0, 0.05) 10px) #fff'" + rgba(0, 0, 0, 0.05) 10px) var(--workflow-graph-color-surface)'" [style.display]="'flex'" [style.flex-direction]="'column'" [style.border]="(selectedNode?.node === node ? 3 : 1) + 'px solid rgb(26, 115, 232)'" [style.transition]="'width .25s, height .25s, border .25s'" + [style.color]="'var(--workflow-graph-color-surface-on-surface)'" (focusin)="[$event.preventDefault(), $event.stopPropagation()]" >
@@ -120,7 +121,7 @@
Interact with this node
+

@@ -620,7 +637,7 @@

Interact with this node
+ \ No newline at end of file diff --git a/src/app/directed_acyclic_graph.scss b/src/app/directed_acyclic_graph.scss index 8eb2e45..4209490 100644 --- a/src/app/directed_acyclic_graph.scss +++ b/src/app/directed_acyclic_graph.scss @@ -9,6 +9,7 @@ flex: 1 1 0%; display: inline-block; position: relative; + background: var(--workflow-graph-color-surface); &:not(.no-flex) { width: 100%; height: 100%; @@ -78,7 +79,7 @@ opacity: 0; pointer-events: none; } - background: #f6f7f7; + background: var(--workflow-graph-color-surface-variant); transition: opacity $graph-change-speed; display: flex; align-items: center; diff --git a/src/app/directed_acyclic_graph.spec.ts b/src/app/directed_acyclic_graph.spec.ts index 5f92bd2..3b58bed 100644 --- a/src/app/directed_acyclic_graph.spec.ts +++ b/src/app/directed_acyclic_graph.spec.ts @@ -23,6 +23,9 @@ import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angul import {ScreenshotTest} from '../screenshot_test'; +import {ColorThemeLoader} from './color_theme_loader'; +import {DagStateService} from './dag-state.service'; +import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; import {DirectedAcyclicGraph, DirectedAcyclicGraphModule} from './directed_acyclic_graph'; import {DagNode as Node, type GraphSpec, type NodeRef} from './node_spec'; import {TEST_IMPORTS, TEST_PROVIDERS} from './test_providers'; @@ -38,10 +41,9 @@ describe('Directed Acyclic Graph Renderer', () => { beforeEach(waitForAsync(async () => { await initTestBed({ declarations: [TestComponent], - imports: [DirectedAcyclicGraphModule], + imports: [DirectedAcyclicGraphModule, ColorThemeLoader], }); screenShot = new ScreenshotTest(module.id); - })); describe('UI', () => { @@ -221,6 +223,9 @@ describe('Directed Acyclic Graph Renderer', () => { height: 1200px; width: 1200px; }`], + providers: [ + // STATE_SERVICE_PROVIDER, + ], // TODO: Make this AOT compatible. See b/352713444 jit: true, diff --git a/src/app/directed_acyclic_graph.ts b/src/app/directed_acyclic_graph.ts index f83c373..d9811d6 100644 --- a/src/app/directed_acyclic_graph.ts +++ b/src/app/directed_acyclic_graph.ts @@ -25,6 +25,7 @@ import {Subject, Subscription} from 'rxjs'; import {takeUntil, throttleTime} from 'rxjs/operators'; import {ShortcutService} from './a11y/shortcut.service'; +import {ColorThemeLoader} from './color_theme_loader'; import {DagStateService} from './dag-state.service'; import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; import {baseColors, BLUE_THEME, clampVal, CLASSIC_THEME, createDAGFeatures, createNewSizeConfig, type DagTheme, DEFAULT_LAYOUT_OPTIONS, DEFAULT_THEME, defaultFeatures, defaultZoomConfig, EdgeStyle, type FeatureToggleOptions, generateTheme, getMargin, isPoint, type LayoutOptions, type Logger, MarkerStyle, type MinimapPosition, nanSafePt, NODE_HEIGHT, NODE_WIDTH, NodeState, OrientationMarginConfig, RankAlignment, RankDirection, RankerAlgorithim, SCROLL_STEP_PER_DELTA, SizeConfig, SVG_ELEMENT_SIZE, type ZoomConfig} from './data_types_internal'; @@ -965,6 +966,7 @@ export class DirectedAcyclicGraph implements OnInit, OnDestroy { MatProgressSpinnerModule, MinimapModule, ZoomingLayer, + ColorThemeLoader, ], declarations: [ DirectedAcyclicGraph, diff --git a/src/app/directed_acyclic_graph_raw.scss b/src/app/directed_acyclic_graph_raw.scss index 1b393ca..ca56630 100644 --- a/src/app/directed_acyclic_graph_raw.scss +++ b/src/app/directed_acyclic_graph_raw.scss @@ -51,7 +51,7 @@ &.edge .edge-group { transition: top $graph-change-speed, left $graph-change-speed; &.pending-or-static .line { - stroke: #9ba1a7; + stroke: $pending-or-static-color; } &.animated { .line{ @@ -83,7 +83,7 @@ .pending-or-static { .inner-circle { - fill: #fff; + fill: var(--workflow-graph-base-color-background-white); display: initial; } .outer-circle { @@ -122,8 +122,8 @@ font-weight: bold; padding: 0.5em; overflow: auto; - background-color: #f44336; - color: white; + background-color: var(--workflow-graph-color-status-error); + color: var(--workflow-graph-color-status-on-error); display: flex; align-items: center; text-align: center; @@ -174,7 +174,7 @@ @include offsetCenter; border: 2px dashed; border-radius: 17px; - color: #bdc1c6; + color: var(--workflow-graph-color-outline); transition: width $graph-change-speed, height $graph-change-speed, color 0.25s, transform $graph-change-speed; &:hover, @@ -254,7 +254,7 @@ content: ''; display: block; border-radius: 6px; - background-color: #fff; + background-color: var(--workflow-graph-color-surface); border: 1px solid $node-border-color; bottom: 0; right: 0; @@ -279,10 +279,7 @@ cursor: pointer; &:hover, &:focus-visible { - background-color: transparentize( - $color: $minimap-primary-color, - $amount: 0.95 - ); + background-color: var(--workflow-graph-base-color-blue-transparent-095); } &:focus-visible { outline: none; @@ -302,8 +299,8 @@ left: 2%; transform: translateY(-50%); /* This color is needed to maintain GAR level contrast with #fff text */ - background: #1a73e8; - color: white; + background: var(--workflow-graph-base-color-blue); + color: var(--workflow-graph-color-on-primary); opacity: 1; transition: opacity $graph-change-speed; transition-delay: calc(2 * #{$graph-change-speed}); @@ -318,8 +315,8 @@ left: 5%; transform: translateY(-50%); /* This color is needed to maintain GAR level contrast with #fff text */ - background: #1a73e8; - color: white; + background: var(--workflow-graph-base-color-blue); + color: var(--workflow-graph-color-on-primary); opacity: 1; transition: opacity $graph-change-speed; transition-delay: calc(2 * #{$graph-change-speed}); @@ -345,7 +342,7 @@ transform: translate(50%, -50%); border: 2px solid; border-radius: 50%; - background: #fff; + background: var(--workflow-graph-color-surface); opacity: 0; transition: border-color 0.25s, color 0.25s, opacity 0.25s; > .caret { @@ -382,7 +379,8 @@ @include offsetCenter; display: inline-block; padding: 1px 2px; - background: white; + background: var(--workflow-graph-color-surface); + color: var(--workflow-graph-color-surface-on-surface); border-radius: 20px; text-overflow: ellipsis; overflow: hidden; diff --git a/src/app/group_iteration_select.scss b/src/app/group_iteration_select.scss index 2e74d3e..8cc5511 100644 --- a/src/app/group_iteration_select.scss +++ b/src/app/group_iteration_select.scss @@ -8,11 +8,11 @@ /* stylelint-disable no-unknown-animations -- All of the animations are imported from mixins.scss */ /* Colors */ -$select-menu-filter-bg: rgb(250, 250, 250); -$select-menu-divider-color: rgba(0, 0, 0, 0.12); -$select-menu-selected-bg: rgb(230, 236, 250); -$select-menu-selected-color: rgb(28, 58, 169); -$select-menu-group-title-color: rgba(0, 0, 0, 0.54); +$select-menu-filter-bg: var(--workflow-graph-color-surface-elevation-1); +$select-menu-divider-color: var(--workflow-graph-color-outline); +$select-menu-selected-bg: var(--workflow-graph-color-primary-container); +$select-menu-selected-color: var(--workflow-graph-color-on-primary); +$select-menu-group-title-color: var(--workflow-graph-color-surface-on-surface); :host { cursor: pointer; @@ -21,9 +21,10 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); position: absolute; bottom: 5px; left: 0; - color: #3c4043; transform: translateY(100%); - transition: height 0.25s, border-color 0.25s; + transition: + height 0.25s, + border-color 0.25s; pointer-events: all; &.untab { overflow: hidden; @@ -38,7 +39,7 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); ::ng-deep mat-select.group-iteration-select { width: 100%; - background: white; + background: var(--workflow-graph-color-surface-elevation-1); height: 100%; border-top: 0; border: 1px solid $node-border-color; @@ -73,7 +74,7 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); } .mat-mdc-select-arrow svg { - fill: #3c4043; + fill: var(--workflow-graph-color-surface-on-surface); } .content-wrap { @@ -91,7 +92,7 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); flex: 1 1 0%; font-weight: $ace-font-weight-medium; font-size: 14px; - color: #3c4043; + color: var(--workflow-graph-color-surface-on-surface); } .empty-state { @@ -111,7 +112,7 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); ::ng-deep div.ai-dag-iteration-select-panel.mat-mdc-select-panel { min-width: 300px; padding: 0; - background: white; + background: var(--workflow-graph-color-surface-elevation-1); .ai-dag-iteration-select-filter { z-index: 1; @@ -127,7 +128,16 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); position: sticky; top: 0; + .ai-dag-iteration-selector-filter-label { + color: var(--workflow-graph-color-surface-on-surface); + } + + .workflow-graph-icon-filter { + color: var(--workflow-graph-color-surface-on-surface); + } + input { + color: var(--workflow-graph-color-surface-on-surface); background-color: transparent; border: 0px; min-width: 0; @@ -142,6 +152,7 @@ $select-menu-group-title-color: rgba(0, 0, 0, 0.54); width: 14px; height: 14px; line-height: 14px; + color: var(--workflow-graph-color-surface-on-surface); mat-icon { width: 14px; height: 14px; diff --git a/src/app/icon_util.ts b/src/app/icon_util.ts index 5a326a2..862df74 100644 --- a/src/app/icon_util.ts +++ b/src/app/icon_util.ts @@ -29,8 +29,8 @@ export const iconConfigDefaults = { name: '', iconset: 'common', iconColors: 'normal', - color: 'black', - contrastColor: 'white', + color: 'var(--workflow-graph-base-color-black)', + contrastColor: 'var(--workflow-graph-color-surface)', font: '', get size() { return sizeMap[(this as unknown as NodeIcon).iconset!] || 'small'; diff --git a/src/app/material_styles_loader.scss b/src/app/material_styles_loader.scss index 74c776e..b45ab83 100644 --- a/src/app/material_styles_loader.scss +++ b/src/app/material_styles_loader.scss @@ -1,3 +1,10 @@ @use 'material_theme'; @include material_theme.graph-material-styles(); +@layer styles; + +@layer styles { + .workflow-graph-theme-dark { + @include material_theme.graph-material-dark-mode-colors(); + } +} diff --git a/src/app/material_theme.scss b/src/app/material_theme.scss index ab7facd..6e8c0c6 100644 --- a/src/app/material_theme.scss +++ b/src/app/material_theme.scss @@ -18,6 +18,17 @@ $graph-theme: mat.define-light-theme( ) ); +$graph-dark-theme: mat.define-dark-theme( + ( + color: ( + primary: $my-primary, + accent: $my-accent, + ), + typography: mat.define-typography-config(), + density: 0, + ) +); + @mixin graph-material-styles() { // Include theme styles for core and each component used in your app. @include mat.core-theme($graph-theme); @@ -34,3 +45,7 @@ $graph-theme: mat.define-light-theme( @include mat.slide-toggle-theme($graph-theme); @include mat.button-toggle-theme($graph-theme); } + +@mixin graph-material-dark-mode-colors { + @include mat.all-component-colors($graph-dark-theme); +} diff --git a/src/app/minimap/minimap.scss b/src/app/minimap/minimap.scss index fcf242a..81118d8 100644 --- a/src/app/minimap/minimap.scss +++ b/src/app/minimap/minimap.scss @@ -37,7 +37,7 @@ rgba(#000, 0.05) 2px, rgba(#000, 0.05) 4px ) - white; + var(--workflow-graph-color-surface-elevation-1); outline: 1px solid $minimap-primary-color; border-radius: 4px; overflow: hidden; @@ -49,7 +49,7 @@ .bounds { pointer-events: none; width: 100%; - background: white; + background: var(--workflow-graph-color-surface); overflow: hidden; .dag-preview { transform-origin: 0 0; diff --git a/src/app/minimap/minimap.spec.ts b/src/app/minimap/minimap.spec.ts index cdd833f..5fb0609 100644 --- a/src/app/minimap/minimap.spec.ts +++ b/src/app/minimap/minimap.spec.ts @@ -20,6 +20,7 @@ import {Component, ViewChild} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, waitForAsync} from '@angular/core/testing'; import {ScreenshotTest} from '../../screenshot_test'; +import {ColorThemeLoader} from '../color_theme_loader'; import {STATE_SERVICE_PROVIDER} from '../dag-state.service.provider'; import {MinimapPosition} from '../data_types_internal'; import {GraphSpec} from '../node_spec'; @@ -43,7 +44,7 @@ describe('Minimap', () => { beforeEach(waitForAsync(async () => { await initTestBed({ declarations: [TestComponent], - imports: [MinimapModule], + imports: [MinimapModule, ColorThemeLoader], providers: [STATE_SERVICE_PROVIDER], }); screenShot = new ScreenshotTest(module.id); @@ -250,11 +251,13 @@ describe('Minimap', () => { [x]="x" [y]="y" /> +
`, styles: [` .container { width: 150px; }`], + providers: [STATE_SERVICE_PROVIDER], jit: true, }) class TestComponent { diff --git a/src/app/minimap/scuba_goldens/minimap/chrome-linux/expanded_groups.png b/src/app/minimap/scuba_goldens/minimap/chrome-linux/expanded_groups.png index a76690f..39f74e9 100644 Binary files a/src/app/minimap/scuba_goldens/minimap/chrome-linux/expanded_groups.png and b/src/app/minimap/scuba_goldens/minimap/chrome-linux/expanded_groups.png differ diff --git a/src/app/minimap/scuba_goldens/minimap/chrome-linux/renders_correctly.png b/src/app/minimap/scuba_goldens/minimap/chrome-linux/renders_correctly.png index 443e44c..d4fef8b 100644 Binary files a/src/app/minimap/scuba_goldens/minimap/chrome-linux/renders_correctly.png and b/src/app/minimap/scuba_goldens/minimap/chrome-linux/renders_correctly.png differ diff --git a/src/app/mixins.scss b/src/app/mixins.scss index 0b1d667..b0c41c6 100644 --- a/src/app/mixins.scss +++ b/src/app/mixins.scss @@ -1,11 +1,11 @@ /* Colors */ -$active-color: #3b78e7; -$pending-or-static-color: #9ba1a7; -$edge-label: #2b2b2b; -$minimap-primary-color: #4285f4; +$active-color: var(--workflow-graph-base-color-blue); +$pending-or-static-color: var(--workflow-graph-color-state-pending); +$edge-label: var(--workflow-graph-color-surface-on-surface); +$minimap-primary-color: var(--workflow-graph-base-color-blue); $caret-space: 6px; $graph-change-speed: 0.15s; -$node-border-color: #263c4043; +$node-border-color: var(--workflow-graph-color-outline); $toolbar-height: 36px; $pulse-color: $active-color; $ace-font-weight-medium: 500; @@ -41,26 +41,26 @@ $default-font: Roboto, 'Helvetica Neue', sans-serif; @keyframes pulse { 0% { - box-shadow: 0 0 0 0 transparentize($color: $pulse-color, $amount: 0.5); + box-shadow: 0 0 0 0 var(--workflow-graph-base-color-blue-transparent-05); } 60% { - box-shadow: 0 0 0 10px transparentize($color: $pulse-color, $amount: 1); + box-shadow: 0 0 0 10px var(--workflow-graph-base-color-blue-transparent-1) } 100% { - box-shadow: 0 0 0 0 transparentize($color: $pulse-color, $amount: 1); + box-shadow: 0 0 0 0 var(--workflow-graph-base-color-blue-transparent-1) } } @keyframes pulse-inset { 0% { - box-shadow: inset 0 0 0 0 transparentize($color: $pulse-color, $amount: 0.5); + box-shadow: inset 0 0 0 0 var(--workflow-graph-base-color-blue-transparent-05); } 60% { box-shadow: inset 0 0 0 20px - transparentize($color: $pulse-color, $amount: 1); + var(--workflow-graph-base-color-blue-transparent-1) } 100% { - box-shadow: inset 0 0 0 0 transparentize($color: $pulse-color, $amount: 1); + box-shadow: inset 0 0 0 0 var(--workflow-graph-base-color-blue-transparent-1) } } diff --git a/src/app/node.scss b/src/app/node.scss index 8464c09..11c4648 100644 --- a/src/app/node.scss +++ b/src/app/node.scss @@ -19,7 +19,7 @@ .node-main:after { content: ''; pointer-events: none; - background-color: transparentize($color: $active-color, $amount: 0.88); + background-color: var(--workflow-graph-base-color-blue-transparent-088); display: block; border-radius: 6px; @include fullbleed; @@ -37,31 +37,28 @@ // stylelint-disable-next-line declaration-no-important -- This needs to be // there to allow proper styling of conditional icons (that are injected via // JavaScript). See go/dag-component#heading=h.d2s34awaiaeo - color: #bdc1c6 !important; + color: var(--workflow-graph-color-state-disabled) !important; } &[state='pending'], &[runtime='static'] .node-main { - .node-title { - color: #8a8f94; - } .icon-space.node-type { &.inverted { // stylelint-disable-next-line declaration-no-important -- This needs to // be there to allow proper styling of icon colors (that are injected // via JavaScript). See go/dag-component#heading=h.d2s34awaiaeo - background: #e8eaed !important; + background: var(--workflow-graph-color-neutral-container) !important; .icon.left { - color: #9ba1a7; + color: var(--workflow-graph-color-surface-on-surface-variant); } } .icon.left { - color: #bdc1c6; + color: var(--workflow-graph-color-state-disabled); } } } &[type='execution'][runtime='static'] .node-main { - background: #f7f7f7; + background: var(--workflow-graph-color-surface-variant); } &[type='artifact'][runtime='static'] .node-main { @@ -85,7 +82,7 @@ } } .node-main { - background: white; + background: var(--workflow-graph-color-surface); border: 1px solid $node-border-color; border-radius: 6px; box-sizing: content-box; @@ -127,14 +124,14 @@ overflow: hidden; } .node-title { - color: #3c4043; + color: var(--workflow-graph-color-surface-on-surface); font-weight: $ace-font-weight-medium; font-size: 14px; line-height: 1.4; transition: color 0.25s; } .description-text { - color: #6a727c; + color: var(--workflow-graph-color-surface-on-surface-variant); font-size: 12px; line-height: 1.3; } @@ -210,13 +207,6 @@ } } -:not(:host(.hovered)) .text-area .callout { - // stylelint-disable-next-line declaration-no-important -- This needs to - // be there to allow proper styling of callout colors (that are injected - // via JavaScript). - color: transparent !important; -} - :host(.hovered) .text-area .callout { transition: font-size 0.25s ease 0.25s, border-radius 0.25s ease 0s, bottom 0.25s ease 0s, right 0.25s ease 0s, color 0.25s ease 0s, diff --git a/src/app/node.ts b/src/app/node.ts index 5d41602..37e9402 100644 --- a/src/app/node.ts +++ b/src/app/node.ts @@ -157,8 +157,8 @@ export class DagNodeEl implements OnInit, OnDestroy { /** Get different properties from the node callout */ getCallout(node: DagNode, prop: 'text'|'color'|'bg') { const defaultVal = { - color: 'gray', - bg: '#dad4d4', + color: 'var(--workflow-graph-base-color-gray)', + bg: 'var(--workflow-graph-hairline)', }; const {callout} = node; if (typeof callout === 'string') { diff --git a/src/app/node_ref_badge.scss b/src/app/node_ref_badge.scss index ea7f69b..de94191 100644 --- a/src/app/node_ref_badge.scss +++ b/src/app/node_ref_badge.scss @@ -3,7 +3,7 @@ */ /* Mixin and deps imports */ -@import './mixins.scss'; +@import 'mixins'; /* stylelint-disable no-unknown-animations -- All of the animations are imported from mixins.scss */ @@ -46,16 +46,16 @@ // stylelint-disable-next-line declaration-no-important -- This needs to // be there to allow proper styling of icon colors (that are injected // via JavaScript). See go/dag-component#heading=h.d2s34awaiaeo - background: #e8eaed !important; + background: var(--workflow-graph-color-surface-variant) !important; // stylelint-disable-next-line declaration-no-important -- This needs to // be there to allow proper styling of icon colors (that are injected // via JavaScript). See go/dag-component#heading=h.d2s34awaiaeo - color: #9ba1a7 !important; + color: var(--workflow-graph-color-surface-on-surface-variant) !important; border-color: $node-border-color; } &[type='execution'][runtime='static'] .scale-wrap { - background: #f7f7f7; + background: var(--workflow-graph-color-surface-variant); } &[type='artifact'][runtime='static'] .scale-wrap { diff --git a/src/app/node_state_badge.ng.html b/src/app/node_state_badge.ng.html index 0aea191..5206a54 100644 --- a/src/app/node_state_badge.ng.html +++ b/src/app/node_state_badge.ng.html @@ -16,3 +16,4 @@ {{ labelForState(nodeState) }} + \ No newline at end of file diff --git a/src/app/node_state_badge.scss b/src/app/node_state_badge.scss index eb679eb..b821bf3 100644 --- a/src/app/node_state_badge.scss +++ b/src/app/node_state_badge.scss @@ -1,6 +1,7 @@ /* Mixin and deps imports */ @import './mixins'; + .ai-dag-node-state-badge { align-items: center; border-radius: 12px; diff --git a/src/app/node_state_badge.ts b/src/app/node_state_badge.ts index cc7f744..0a75943 100644 --- a/src/app/node_state_badge.ts +++ b/src/app/node_state_badge.ts @@ -18,6 +18,7 @@ import {CommonModule} from '@angular/common'; import {Component, Input, NgModule} from '@angular/core'; +import {ColorThemeLoader} from './color_theme_loader'; import {DEFAULT_THEME, IconConfig, isNoState, type NodeState} from './data_types_internal'; import {TranslationsService} from './i18n'; import {bgForState, fetchIcon, iconForState, labelForState} from './icon_util'; @@ -53,6 +54,7 @@ export {type NodeState}; @NgModule({ imports: [ CommonModule, + ColorThemeLoader, WorkflowGraphIconModule, ], declarations: [ diff --git a/src/app/scaffold.ng.html b/src/app/scaffold.ng.html index 513c92d..62264be 100644 --- a/src/app/scaffold.ng.html +++ b/src/app/scaffold.ng.html @@ -2,4 +2,7 @@
- + + diff --git a/src/app/scaffold.ts b/src/app/scaffold.ts index d6a8eb3..97801bb 100644 --- a/src/app/scaffold.ts +++ b/src/app/scaffold.ts @@ -16,11 +16,12 @@ */ import {NgIf} from '@angular/common'; -import {AfterContentInit, ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, NgModule, OnDestroy, Output, ViewEncapsulation} from '@angular/core'; +import {AfterContentInit, ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, NgModule, OnDestroy, Output, signal, ViewEncapsulation} from '@angular/core'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {ShortcutService} from './a11y/shortcut.service'; +import {ColorThemeLoader} from './color_theme_loader'; import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; import {baseColors, BLUE_THEME, createDAGFeatures, type DagTheme, DEFAULT_THEME, defaultFeatures, type FeatureToggleOptions, generateTheme} from './data_types_internal'; import {DirectedAcyclicGraph} from './directed_acyclic_graph'; @@ -130,6 +131,7 @@ export class DagScaffold implements AfterContentInit, OnDestroy { imports: [ DagLoggerModule, MaterialStylesLoader, + ColorThemeLoader, NgIf, ], exports: [ diff --git a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_custom_control_node.png b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_custom_control_node.png index b06d6cf..50f7e7f 100644 Binary files a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_custom_control_node.png and b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_custom_control_node.png differ diff --git a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_expanded_with_custom_control_node_hidden.png b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_expanded_with_custom_control_node_hidden.png index ee81a74..0055ec2 100644 Binary files a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_expanded_with_custom_control_node_hidden.png and b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_expanded_with_custom_control_node_hidden.png differ diff --git a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_loading.png b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_loading.png index e7b2234..8ab7982 100644 Binary files a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_loading.png and b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_loading.png differ diff --git a/src/app/scuba_goldens/toolbar/chrome-linux/renders_correctly.png b/src/app/scuba_goldens/toolbar/chrome-linux/renders_correctly.png index 805fe2e..debc56f 100644 Binary files a/src/app/scuba_goldens/toolbar/chrome-linux/renders_correctly.png and b/src/app/scuba_goldens/toolbar/chrome-linux/renders_correctly.png differ diff --git a/src/app/test_resources/demo_page_harness.ts b/src/app/test_resources/demo_page_harness.ts index 8c247ab..b6dccd1 100644 --- a/src/app/test_resources/demo_page_harness.ts +++ b/src/app/test_resources/demo_page_harness.ts @@ -26,4 +26,9 @@ export class DemoPageHarness extends ComponentHarness { return this.locatorFor( MatNativeSelectHarness.with({selector: '#dataset-select'}))(); } + + getColorThemeInput(): Promise { + return this.locatorFor( + MatNativeSelectHarness.with({selector: '#color-theme-select'}))(); + } } diff --git a/src/app/toolbar.ng.html b/src/app/toolbar.ng.html index c54534d..0bc5b3c 100644 --- a/src/app/toolbar.ng.html +++ b/src/app/toolbar.ng.html @@ -184,3 +184,4 @@ + \ No newline at end of file diff --git a/src/app/toolbar.scss b/src/app/toolbar.scss index 8eec576..5bb123e 100644 --- a/src/app/toolbar.scss +++ b/src/app/toolbar.scss @@ -3,14 +3,12 @@ */ /* Mixin and deps imports */ -@import './mixins'; - -$toolbar-color: #5f6368; +@import 'mixins'; @mixin text-styles { font-size: 12.5px; font-weight: $ace-font-weight-medium; - color: $toolbar-color; + color: var(--workflow-graph-color-non-primary); } @mixin disable-selection { @@ -38,7 +36,7 @@ $toolbar-color: #5f6368; border-bottom: 1px solid rgba(0, 0, 0, 0.12); z-index: 1; font-family: $default-font; - + background: var(--workflow-graph-color-surface); .flex { flex: 1 0 0%; } @@ -52,13 +50,13 @@ $toolbar-color: #5f6368; display: flex; align-items: center; &:not(.right) { - border-right: 1px solid #dbdde1; + border-right: 1px solid var(--workflow-graph-color-hairline); } &.right { - border-left: 1px solid #dbdde1; + border-left: 1px solid var(--workflow-graph-color-hairline); } > workflow-graph-icon { - color: #80868b; + color: var(--workflow-graph-color-non-primary); transition: color 0.25s; margin: 0 0.25em 0 0; } @@ -80,7 +78,7 @@ $toolbar-color: #5f6368; width: 28px; height: 28px; line-height: 28px; - color: $toolbar-color; + color: var(--workflow-graph-color-non-primary); padding: 0; border: 0; diff --git a/src/app/toolbar.spec.ts b/src/app/toolbar.spec.ts index 698be82..d8bdd68 100644 --- a/src/app/toolbar.spec.ts +++ b/src/app/toolbar.spec.ts @@ -22,6 +22,7 @@ import {ComponentFixture, fakeAsync, flush, TestBed, waitForAsync} from '@angula import {ScreenshotTest} from '../screenshot_test'; +import {ColorThemeLoader} from './color_theme_loader'; import {DagStateService} from './dag-state.service'; import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; import {defaultFeatures} from './data_types_internal'; @@ -43,7 +44,8 @@ describe('DagToolbar', () => { beforeEach(waitForAsync(async () => { await initTestBed({ declarations: [TestComponent], - imports: [DagToolbarModule], + imports: [DagToolbarModule, ColorThemeLoader], + providers: [STATE_SERVICE_PROVIDER], }); })); @@ -162,15 +164,15 @@ describe('DagToolbar', () => { `, providers: [ - STATE_SERVICE_PROVIDER, + // STATE_SERVICE_PROVIDER, ], styles: [` .container { height: 400px; width: 400px; }`], -// TODO: Make this AOT compatible. See b/352713444 -jit: true, + // TODO: Make this AOT compatible. See b/352713444 + jit: true, }) class TestComponent { diff --git a/src/app/toolbar.ts b/src/app/toolbar.ts index 41d7887..0255b38 100644 --- a/src/app/toolbar.ts +++ b/src/app/toolbar.ts @@ -21,6 +21,7 @@ import {FormsModule} from '@angular/forms'; import {AccessibilityHelpCenter} from './a11y/a11y_help_center'; import {ShortcutService} from './a11y/shortcut.service'; +import {ColorThemeLoader} from './color_theme_loader'; import {DagStateService} from './dag-state.service'; import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; import {baseColors, BLUE_THEME, clampVal, createDAGFeatures, type DagTheme, DEFAULT_THEME, defaultFeatures, defaultZoomConfig, type FeatureToggleOptions, generateTheme, isNoState, RuntimeState, type ZoomConfig} from './data_types_internal'; @@ -368,6 +369,7 @@ export class DagToolbar { @NgModule({ imports: [ CommonModule, + ColorThemeLoader, WorkflowGraphIconModule, FormsModule, NgVarModule,