From c86618ecd385dcd237f819447d5e6185088b70d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Carvalho?= <42584819+Lcarv20@users.noreply.github.com> Date: Thu, 31 Mar 2022 05:52:27 +0200 Subject: [PATCH] feat(dep-graph): add dark mode (#8712) * feat(dep-graph): add dark mode Squashed commits: feat(dep-graph): Updating tailwind config file. As of version 3.0 higher tailwind jit mode is default. Instead of purge is now content and as dark mode will be added as class. feat(dep-graph): Enabling toggable dark mode. As darkmode (tailwind.congig.json) is set as class, we need to add the script to the head tag on index.html. see: https://tailwindcss.com/docs/dark-mode feat(dep-graph): Adding theme Initializer and creating support functions All functions related to theme changing and initialization are located on the theme-resolver.tsx The themeInit function will run when page is loaded and apply add class to the tag as dark or light accordignly. Theme resolver applies the changes and keeps localstorage and in sync. feat(dep-graph): Adding theme panel The pannel allows users to switch themes. Currently it only changes the html class and localstorage. feat(dep-graph): Creating tailwind dark colors pallete The current colors remain as light. The dark colors are an attempt to contrast the light ones. feat(dep-graph): Adding additional styles to sidebar feat(dep-graph): Define styling presets for darkmode to allow consistent and concise classNames. feat(dep-graph): Apply dark styling classes to sidebar feat(dep-graph): Added dark mode styles to the debugger feat(dep-graph): Added Color to tailwind config and adjusting imports. feat(dep-graph): Created theme tracker feat(dep-graph): Added dark classes to graph container feat(dep-graph): Adjusted some edge styles for better UX. Added dynamic selection of colors according to the current theme. feat(dep-graph): Added transition when switching themes. feat(dep-graph): Readded auto roation for implicit label and dynamic background. feat(dep-graph): Assigned generic types to selectDynbamically, and added new color to the pallete. feat(dep-graph): Added dynamic styles for theming. feat(dep-graph): Added mock for matchMedia. Tests will fail otherwise. feat(dep-graph): Added styles to tippy. feat(dep-graph): Moved theme related functions to theme-resolver file feat(dep-graph): Implement dark mode on tooltips. feat(dep-graph): re-evaluate graph colors on theme change cleanup(dep-graph): Removed duplicate style chore(dep-graph): Testing theme preferences Adding test cases for theme initialization, and the ability to change. cleanup(dep-graph): removing repeated style classes Fixed issue with webpack plugin (#8231) feat(npm): resolved issue with live reload failing Fixed the issue with live reload when adding scripts in project.json Closes #8230 chore(repo): update nx to 13.10.0-beta.1 (#9407) feat(dep-graph): re-evaluate graph colors on theme change fix(dep-graph): use theme background color in image download * fix(dep-graph): change dark mode styles * cleanup(dep-graph): cleanup e2e tests and naming Co-authored-by: Philip Fulcher --- .../client-e2e/src/integration/app.spec.ts | 44 ++++++++++++ dep-graph/client/jest.config.js | 5 ++ dep-graph/client/project.json | 41 ++++++----- dep-graph/client/src/app/app.tsx | 3 + .../client/src/app/dark-theme-styles.tsx | 8 +++ dep-graph/client/src/app/debugger-panel.tsx | 24 ++++--- .../client/src/app/experimental-feature.tsx | 9 +-- .../src/app/hooks/use-environment-config.ts | 12 ++-- .../client/src/app/machines/dep-graph.spec.ts | 8 +-- dep-graph/client/src/app/machines/graph.ts | 13 +++- .../src/app/machines/match-media-mock.spec.ts | 14 ++++ dep-graph/client/src/app/shell.tsx | 17 ++++- .../src/app/sidebar/focused-project-panel.tsx | 5 +- .../src/app/sidebar/group-by-folder-panel.tsx | 4 +- dep-graph/client/src/app/sidebar/modal.tsx | 42 +++++++++++ .../client/src/app/sidebar/project-list.tsx | 23 +++++-- .../client/src/app/sidebar/search-depth.tsx | 11 +-- .../src/app/sidebar/show-hide-projects.tsx | 7 +- dep-graph/client/src/app/sidebar/sidebar.tsx | 43 +++++++++--- .../src/app/sidebar/text-filter-panel.tsx | 11 +-- .../client/src/app/sidebar/theme-icons.tsx | 60 ++++++++++++++++ .../client/src/app/sidebar/theme-panel.tsx | 39 +++++++++++ .../client/src/app/styles-graph/edges.ts | 24 +++++-- .../client/src/app/styles-graph/nodes.ts | 30 ++++---- .../client/src/app/styles-graph/palette.ts | 1 + dep-graph/client/src/app/theme-resolver.tsx | 69 +++++++++++++++++++ dep-graph/client/src/app/tooltip-service.ts | 3 +- dep-graph/client/src/index.html | 16 +++++ dep-graph/client/src/styles.scss | 21 +++++- dep-graph/client/tailwind.config.js | 17 ++++- scripts/copy-dep-graph-environment.ts | 15 ++-- 31 files changed, 528 insertions(+), 111 deletions(-) create mode 100644 dep-graph/client/src/app/dark-theme-styles.tsx create mode 100644 dep-graph/client/src/app/machines/match-media-mock.spec.ts create mode 100644 dep-graph/client/src/app/sidebar/modal.tsx create mode 100644 dep-graph/client/src/app/sidebar/theme-icons.tsx create mode 100644 dep-graph/client/src/app/sidebar/theme-panel.tsx create mode 100644 dep-graph/client/src/app/theme-resolver.tsx diff --git a/dep-graph/client-e2e/src/integration/app.spec.ts b/dep-graph/client-e2e/src/integration/app.spec.ts index 8b3ab792c28d0..6dc020cf6c055 100644 --- a/dep-graph/client-e2e/src/integration/app.spec.ts +++ b/dep-graph/client-e2e/src/integration/app.spec.ts @@ -266,3 +266,47 @@ describe('loading dep-graph client with url params', () => { getCheckedProjectItems().should('have.length', 53); }); }); + +describe('theme preferences', () => { + let systemTheme: string; + beforeEach(() => { + cy.visit('/'); + systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + }); + it('should initialize localstorage with default theme', () => { + expect(localStorage.getItem('nx-dep-graph-theme')).eq('system'); + }); + + it('has system default theme', () => { + cy.log('system theme is:', systemTheme); + cy.get('html').should('have.class', systemTheme); + }); + + describe('dark theme is set as prefered', () => { + before(() => { + cy.get('[data-cy="theme-open-modal-button"]').click(); + cy.get('[data-cy="dark-theme-button"]').click(); + }); + + it('should set dark theme', () => { + cy.log('Localstorage is: ', localStorage.getItem('nx-dep-graph-theme')); + expect(localStorage.getItem('nx-dep-graph-theme')).eq('dark'); + cy.get('html').should('have.class', 'dark'); + }); + }); + + describe('light theme is set as preferred', () => { + before(() => { + cy.get('[data-cy="theme-open-modal-button"]').click(); + cy.get('[data-cy="light-theme-button"]').click(); + }); + + it('should set light theme', () => { + cy.log('Localstorage is: ', localStorage.getItem('nx-dep-graph-theme')); + expect(localStorage.getItem('nx-dep-graph-theme')).eq('light'); + cy.get('html').should('have.class', 'light'); + }); + }); +}); diff --git a/dep-graph/client/jest.config.js b/dep-graph/client/jest.config.js index 313faa823dfa6..6c5973b3e0314 100644 --- a/dep-graph/client/jest.config.js +++ b/dep-graph/client/jest.config.js @@ -10,4 +10,9 @@ module.exports = { }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], coverageDirectory: '../../coverage/nx-dev/nx-dev', + // The mock for widnow.matchMedia has to be in a separete file and imported before the components to test + // for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom + modulePathIgnorePatterns: [ + '/dep-graph/client/src/app/machines/match-media-mock.spec.ts', + ], }; diff --git a/dep-graph/client/project.json b/dep-graph/client/project.json index c9633da3b901d..bedd22828f0d8 100644 --- a/dep-graph/client/project.json +++ b/dep-graph/client/project.json @@ -15,20 +15,7 @@ "styles": ["dep-graph/client/src/styles.scss"], "scripts": [], "assets": [], - "optimization": true, - "outputHashing": "none", - "sourceMap": false, - "extractCss": true, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ], + "webpackConfig": "dep-graph/client/webpack.config.js" }, "configurations": { @@ -57,15 +44,35 @@ "maximumError": "5mb" } ] + }, + "release": { + "optimization": true, + "outputHashing": "none", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] } }, + "defaultConfiguration": "release", "outputs": ["{options.outputPath}"] }, "serve-base": { "executor": "@nrwl/web:dev-server", - "options": { - "buildTarget": "dep-graph-client:build-base:dev" - } + "configurations": { + "dev": { + "buildTarget": "dep-graph-client:build-base:dev" + } + }, + "defaultConfiguration": "dev" }, "lint": { "executor": "@nrwl/linter:eslint", diff --git a/dep-graph/client/src/app/app.tsx b/dep-graph/client/src/app/app.tsx index 6ed3388031724..1ae8d7ede3b12 100644 --- a/dep-graph/client/src/app/app.tsx +++ b/dep-graph/client/src/app/app.tsx @@ -1,5 +1,8 @@ import { Shell } from './shell'; import { GlobalStateProvider } from './state.provider'; +import { themeInit } from './theme-resolver'; + +themeInit(); export function App() { return ( diff --git a/dep-graph/client/src/app/dark-theme-styles.tsx b/dep-graph/client/src/app/dark-theme-styles.tsx new file mode 100644 index 0000000000000..903bd7f098728 --- /dev/null +++ b/dep-graph/client/src/app/dark-theme-styles.tsx @@ -0,0 +1,8 @@ +// Styling presets for dark mode, defined by agregating styling classes +export const DarkClasses = { + button: + 'dark:bg-sidebar-btn-dark dark:border-sidebar-border-dark hover:dark:bg-opacity-40 dark:text-sidebar-text-dark', + buttonAffected: 'dark:bg-sidebar-btn-dark hover:dark:bg-red-900/[.05]', + input: + 'dark:text-sidebar-text-dark dark:bg-sidebar-btn-dark/[0.5] dark:border-sidebar-border-dark', +}; diff --git a/dep-graph/client/src/app/debugger-panel.tsx b/dep-graph/client/src/app/debugger-panel.tsx index 383acb80f9279..0db3c1f351842 100644 --- a/dep-graph/client/src/app/debugger-panel.tsx +++ b/dep-graph/client/src/app/debugger-panel.tsx @@ -1,6 +1,7 @@ import { ProjectGraphList } from './interfaces'; import { GraphPerfReport } from './machines/interfaces'; import { memo } from 'react'; +import { DarkClasses } from './dark-theme-styles'; export interface DebuggerPanelProps { projectGraphs: ProjectGraphList[]; @@ -20,19 +21,26 @@ export const DebuggerPanel = memo(function ({ data-cy="debugger-panel" className=" flex-column + flex-column + dark:bg-sidebar-dark + dark:border-sidebar-border-dark flex flex - w-auto - items-center justify-items-center + w-auto items-center + items-center + justify-items-center + justify-items-center gap-4 gap-4 - border-b border-gray-200 + border-b + border-gray-200 bg-gray-50 - p-4 - text-gray-700 + p-4 transition-all " > -

Debugger

+

+ Debugger +

-

+

Last render took {lastPerfReport.renderTime}ms:{' '} {lastPerfReport.numNodes} nodes{' '} |{' '} diff --git a/dep-graph/client/src/app/experimental-feature.tsx b/dep-graph/client/src/app/experimental-feature.tsx index 59406c31df04f..bdea528aeb2be 100644 --- a/dep-graph/client/src/app/experimental-feature.tsx +++ b/dep-graph/client/src/app/experimental-feature.tsx @@ -5,14 +5,7 @@ function ExperimentalFeature(props) { const showExperimentalFeatures = environment.appConfig.showExperimentalFeatures; - return showExperimentalFeatures ? ( -

-

- Experimental Features -

- {props.children} -
- ) : null; + return showExperimentalFeatures ? props.children : null; } export default ExperimentalFeature; diff --git a/dep-graph/client/src/app/hooks/use-environment-config.ts b/dep-graph/client/src/app/hooks/use-environment-config.ts index 1d43862147bf3..d1dd91b43169c 100644 --- a/dep-graph/client/src/app/hooks/use-environment-config.ts +++ b/dep-graph/client/src/app/hooks/use-environment-config.ts @@ -12,7 +12,13 @@ export function useEnvironmentConfig(): { appConfig: AppConfig; useXstateInspect: boolean; } { - const environmentConfig = useRef({ + const environmentConfig = useRef(getEnvironmentConfig()); + + return environmentConfig.current; +} + +export function getEnvironmentConfig() { + return { exclude: window.exclude, watch: window.watch, localMode: window.localMode, @@ -20,7 +26,5 @@ export function useEnvironmentConfig(): { environment: window.environment, appConfig: window.appConfig, useXstateInspect: window.useXstateInspect, - }); - - return environmentConfig.current; + }; } diff --git a/dep-graph/client/src/app/machines/dep-graph.spec.ts b/dep-graph/client/src/app/machines/dep-graph.spec.ts index d6993473fd987..86eec937fc3fc 100644 --- a/dep-graph/client/src/app/machines/dep-graph.spec.ts +++ b/dep-graph/client/src/app/machines/dep-graph.spec.ts @@ -1,12 +1,10 @@ // nx-ignore-next-line -import type { - ProjectGraphDependency, - ProjectGraphProjectNode, -} from '@nrwl/devkit'; +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +import './match-media-mock.spec'; import { depGraphMachine } from './dep-graph.machine'; import { interpret } from 'xstate'; -export const mockProjects: ProjectGraphProjectNode[] = [ +export const mockProjects: ProjectGraphNode[] = [ { name: 'app1', type: 'app', diff --git a/dep-graph/client/src/app/machines/graph.ts b/dep-graph/client/src/app/machines/graph.ts index 7d62f04a72ccc..a25fd73881115 100644 --- a/dep-graph/client/src/app/machines/graph.ts +++ b/dep-graph/client/src/app/machines/graph.ts @@ -18,6 +18,7 @@ import { ProjectNode, } from '../util-cytoscape'; import { GraphRenderEvents, GraphPerfReport } from './interfaces'; +import { selectValueByThemeStatic } from '../theme-resolver'; export class GraphService { private traversalGraph: cy.Core; @@ -557,6 +558,16 @@ export class GraphService { } getImage() { - return this.renderGraph.png({ bg: '#fff', full: true }); + const bg = selectValueByThemeStatic('#262626', '#fff'); + return this.renderGraph.png({ bg, full: true }); + } + + evaluateStyles() { + if (this.renderGraph) { + const container = this.renderGraph.container(); + this.renderGraph.unmount(); + + this.renderGraph.mount(container); + } } } diff --git a/dep-graph/client/src/app/machines/match-media-mock.spec.ts b/dep-graph/client/src/app/machines/match-media-mock.spec.ts new file mode 100644 index 0000000000000..71db5812591a8 --- /dev/null +++ b/dep-graph/client/src/app/machines/match-media-mock.spec.ts @@ -0,0 +1,14 @@ +// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); diff --git a/dep-graph/client/src/app/shell.tsx b/dep-graph/client/src/app/shell.tsx index e171af675a627..93d1057d7165f 100644 --- a/dep-graph/client/src/app/shell.tsx +++ b/dep-graph/client/src/app/shell.tsx @@ -14,6 +14,7 @@ import { projectIsSelectedSelector, } from './machines/selectors'; import Sidebar from './sidebar/sidebar'; +import { selectValueByThemeStatic } from './theme-resolver'; export function Shell() { const depGraphService = useDepGraphService(); @@ -97,7 +98,10 @@ export function Shell() { return ( <> -
+
{environment.appConfig.showDebugger ? ( +
- + + + + + +
+ ); +} diff --git a/dep-graph/client/src/app/sidebar/project-list.tsx b/dep-graph/client/src/app/sidebar/project-list.tsx index 1196015d5d95e..5d9528326d2e1 100644 --- a/dep-graph/client/src/app/sidebar/project-list.tsx +++ b/dep-graph/client/src/app/sidebar/project-list.tsx @@ -62,7 +62,11 @@ function ProjectListItem({ focusProject: (projectId: string) => void; }) { return ( -
  • +