This is an automated email from the ASF dual-hosted git repository.
chanholee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new eee7ebb688 [ZEPPELIN-6323] Apply dark mode to the new UI
eee7ebb688 is described below
commit eee7ebb6887ed6d1e1881396222628a7d2ca9bf8
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Sun Oct 5 17:03:06 2025 +0900
[ZEPPELIN-6323] Apply dark mode to the new UI
### What is this PR for?
This PR adds dark mode support and system theme integration for the
Zeppelin New UI. There were multiple demands such as
[ZEPPELIN-5062](https://issues.apache.org/jira/browse/ZEPPELIN-5602) and
[ZEPPELIN-4024](https://issues.apache.org/jira/browse/ZEPPELIN-4024).
#### Example: Follow system theme + other parts
https://github.com/user-attachments/assets/2159d54e-6403-4f80-91f0-3f66a93881e1
#### Example: Change with button + notebook
https://github.com/user-attachments/assets/59bdf1bc-86a3-42e1-a3d0-1d89d955ed7d
#### Automatic System Theme Detection & Sync
- Automatically detect OS-level dark/light mode settings
- Real-time detection and application of system theme changes
- Theme cycle pattern: `auto(system) → opposite theme → original theme →
auto`
#### Comprehensive Dark Mode UI Support
- Applied dark mode styles across all major components
- Added dark mode overrides for Ant Design components
- Full Monaco Editor dark theme support
- Consistent color scheme and visual hierarchy
#### Enhanced User Experience
- Eliminated FOUC (Flash of Unstyled Content) with logic handled in
`index.html`
- Easy theme switching via a toggle button
- Persisted user preferences in local storage
- Theme state maintained after page reloads
With dark mode support and system theme integration, Zeppelin delivers a
modern, user-friendly experience. Users can either rely on system theme
settings for automatic adaptation or manually select their preferred theme.
### What type of PR is it?
Improvement
### Todos
* [ ] I created a dark mode [background
image](https://github.com/dididy/zeppelin/blob/5d078a40c17e0561202e809da0761016516b86f2/zeppelin-web-angular/src/assets/images/bg-dark.png)
used for login and loading with ChatGPT, but I need to verify whether there
are any copyright issues.
* [ ] Due to the limited environment setup, I wasn’t able to check all the
cases where graphs are rendered. I think we can leave this as a follow-up issue
to work on later.
### What is the Jira issue?
* [[ZEPPELIN-6323](https://issues.apache.org/jira/browse/ZEPPELIN-6323)]
### How should this be tested?
### Screenshots (if appropriate)
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5078 from dididy/feat/darkmode.
Signed-off-by: ChanHo Lee <[email protected]>
---
LICENSE | 1 +
pom.xml | 1 +
zeppelin-web-angular/angular.json | 1 +
zeppelin-web-angular/e2e/models/theme.page.ts | 61 ++++
.../e2e/tests/theme/dark-mode.spec.ts | 130 ++++++++
zeppelin-web-angular/e2e/utils.ts | 3 +-
zeppelin-web-angular/src/app/app.component.ts | 12 +-
zeppelin-web-angular/src/app/languages/load.ts | 12 +-
.../src/app/pages/login/login.component.less | 17 +-
.../interpreter/interpreter.component.less | 24 +-
.../add-paragraph/add-paragraph.component.less | 15 +-
.../pages/workspace/notebook/notebook.component.ts | 5 +-
.../src/app/services/public-api.ts | 1 +
.../src/app/services/theme.service.ts | 161 +++++++++
.../src/app/share/header/header.component.html | 1 +
.../src/app/share/header/header.component.less | 5 +
zeppelin-web-angular/src/app/share/share.module.ts | 2 +
.../share/theme-toggle/theme-toggle.component.html | 19 ++
.../share/theme-toggle/theme-toggle.component.less | 77 +++++
.../share/theme-toggle/theme-toggle.component.ts | 63 ++++
.../src/assets/fonts/MaterialSymbolsOutlined.woff2 | Bin 0 -> 286056 bytes
.../src/assets/fonts/material-symbols-outlined.css | 36 ++
zeppelin-web-angular/src/assets/images/bg-dark.png | Bin 0 -> 1841188 bytes
zeppelin-web-angular/src/index.html | 14 +
zeppelin-web-angular/src/styles/spin.less | 8 +
.../src/styles/theme/dark-theme-overrides.css | 363 +++++++++++++++++++++
.../src/styles/theme/dark/theme-dark.less | 94 +++---
.../src/styles/theme/markdown.less | 16 +-
28 files changed, 1063 insertions(+), 79 deletions(-)
diff --git a/LICENSE b/LICENSE
index 4668af7ee2..3c3f246917 100644
--- a/LICENSE
+++ b/LICENSE
@@ -243,6 +243,7 @@ The text of each license is also included at
licenses/LICENSE-[project]-[version
(Apache 2.0) Embedded MongoDB
(https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo)
(Apache 2.0) s3proxy (https://github.com/gaul/s3proxy)
(Apache 2.0) kubernetes-client
(https://github.com/fabric8io/kubernetes-client)
+ (Apache 2.0) Material Symbols Outlined (https://fonts.google.com/icons)
========================================================================
BSD 3-Clause licenses
diff --git a/pom.xml b/pom.xml
index a992a32cc0..411a12e133 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1028,6 +1028,7 @@
<exclude>**/src/fonts/simple-line*</exclude>
<exclude>**/src/fonts/Source-Code-Pro*</exclude>
<exclude>**/src/fonts/source-code-pro*</exclude>
+
<exclude>**/src/assets/fonts/MaterialSymbolsOutlined.woff2</exclude>
<exclude>**/src/**/**.test.js</exclude>
<exclude>**/e2e/**/**.spec.js</exclude>
<exclude>package-lock.json</exclude>
diff --git a/zeppelin-web-angular/angular.json
b/zeppelin-web-angular/angular.json
index 83f0a378ce..1f669e329d 100644
--- a/zeppelin-web-angular/angular.json
+++ b/zeppelin-web-angular/angular.json
@@ -68,6 +68,7 @@
}
],
"styles": [
+ "src/styles/theme/dark-theme-overrides.css",
"src/styles/theme/dark/antd-dark.less",
"src/styles/theme/light/antd-light.less",
"src/styles.less",
diff --git a/zeppelin-web-angular/e2e/models/theme.page.ts
b/zeppelin-web-angular/e2e/models/theme.page.ts
new file mode 100644
index 0000000000..5285ac4590
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/theme.page.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { expect, Locator, Page } from '@playwright/test';
+
+export class ThemePage {
+ readonly page: Page;
+ readonly themeToggleButton: Locator;
+ readonly rootElement: Locator;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
+ this.rootElement = page.locator('html');
+ }
+
+ async toggleTheme() {
+ await this.themeToggleButton.click();
+ }
+
+ async assertDarkTheme() {
+ await expect(this.rootElement).toHaveClass(/dark/);
+ await expect(this.rootElement).toHaveAttribute('data-theme', 'dark');
+ await expect(this.themeToggleButton).toHaveText('dark_mode');
+ }
+
+ async assertLightTheme() {
+ await expect(this.rootElement).toHaveClass(/light/);
+ await expect(this.rootElement).toHaveAttribute('data-theme', 'light');
+ await expect(this.themeToggleButton).toHaveText('light_mode');
+ }
+
+ async assertSystemTheme() {
+ await expect(this.themeToggleButton).toHaveText('smart_toy');
+ }
+
+ async setThemeInLocalStorage(theme: 'light' | 'dark' | 'system') {
+ await this.page.evaluate(themeValue => {
+ if (typeof window !== 'undefined' && window.localStorage) {
+ window.localStorage.setItem('zeppelin-theme', themeValue);
+ }
+ }, theme);
+ }
+
+ async clearLocalStorage() {
+ await this.page.evaluate(() => {
+ if (typeof window !== 'undefined' && window.localStorage) {
+ window.localStorage.clear();
+ }
+ });
+ }
+}
diff --git a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
new file mode 100644
index 0000000000..4dedf66218
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { expect, test } from '@playwright/test';
+import { ZeppelinHelper } from '../../helper';
+import { ThemePage } from '../../models/theme.page';
+import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
+
+test.describe('Dark Mode Theme Switching', () => {
+ addPageAnnotationBeforeEach(PAGES.SHARE.THEME_TOGGLE);
+ let zeppelinHelper: ZeppelinHelper;
+ let themePage: ThemePage;
+
+ test.beforeEach(async ({ page }) => {
+ zeppelinHelper = new ZeppelinHelper(page);
+ themePage = new ThemePage(page);
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ // Ensure a clean localStorage for each test
+ await themePage.clearLocalStorage();
+ });
+
+ test('Scenario: User can switch to dark mode and persistence is maintained',
async ({ page }) => {
+ // GIVEN: User is on the main page, which starts in 'system' mode by
default (localStorage cleared).
+ await test.step('GIVEN the page starts in system mode', async () => {
+ await themePage.assertSystemTheme(); // Robot icon for system theme
+ });
+
+ // WHEN: Explicitly set theme to light mode for the rest of the test.
+ await test.step('WHEN the user explicitly sets theme to light mode', async
() => {
+ await themePage.setThemeInLocalStorage('light');
+ await page.reload();
+ await zeppelinHelper.waitForZeppelinReady();
+ await themePage.assertLightTheme(); // Now it should be light mode with
sun icon
+ });
+
+ // WHEN: User switches to dark mode by setting localStorage and reloading.
+ await test.step('WHEN the user switches to dark mode', async () => {
+ await themePage.setThemeInLocalStorage('dark');
+ await page.reload();
+ await zeppelinHelper.waitForZeppelinReady();
+ });
+
+ // THEN: The theme changes to dark mode.
+ await test.step('THEN the page switches to dark mode', async () => {
+ await themePage.assertDarkTheme();
+ });
+
+ // AND: User refreshes the page.
+ await test.step('AND the user refreshes the page', async () => {
+ await page.reload();
+ await zeppelinHelper.waitForZeppelinReady();
+ });
+
+ // THEN: Dark mode is maintained after refresh.
+ await test.step('THEN dark mode is maintained after refresh', async () => {
+ await themePage.assertDarkTheme();
+ });
+
+ // AND: User clicks the toggle again to switch back to light mode.
+ await test.step('AND the user clicks the toggle to switch back to light
mode', async () => {
+ await themePage.toggleTheme();
+ });
+
+ // THEN: The theme switches to system mode.
+ await test.step('THEN the theme switches to system mode', async () => {
+ await themePage.assertSystemTheme();
+ });
+ });
+
+ test('Scenario: System Theme and Local Storage Interaction', async ({ page,
context }) => {
+ // Ensure localStorage is clear for each sub-scenario
+ await themePage.clearLocalStorage();
+
+ await test.step('GIVEN: No localStorage, System preference is Light',
async () => {
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ // When no explicit theme is set, it defaults to 'system' mode
+ // Even in system mode with light preference, the icon should be robot
+ await expect(themePage.rootElement).toHaveClass(/light/);
+ await expect(themePage.rootElement).toHaveAttribute('data-theme',
'light');
+ await themePage.assertSystemTheme(); // Should show robot icon
+ });
+
+ await test.step('GIVEN: No localStorage, System preference is Dark
(initial system state)', async () => {
+ await themePage.setThemeInLocalStorage('system');
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ await themePage.assertSystemTheme(); // Robot icon for system theme
+ });
+
+ await test.step("GIVEN: localStorage is 'dark', System preference is
Light", async () => {
+ await themePage.setThemeInLocalStorage('dark');
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ await themePage.assertDarkTheme(); // localStorage should override system
+ });
+
+ await test.step("GIVEN: localStorage is 'system', THEN: Emulate system
preference change to Light", async () => {
+ await themePage.setThemeInLocalStorage('system');
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ await expect(themePage.rootElement).toHaveClass(/light/);
+ await expect(themePage.rootElement).toHaveAttribute('data-theme',
'light');
+ await themePage.assertSystemTheme(); // Robot icon for system theme
+ });
+
+ await test.step("GIVEN: localStorage is 'system', THEN: Emulate system
preference change to Dark", async () => {
+ await themePage.setThemeInLocalStorage('system');
+ await page.emulateMedia({ colorScheme: 'dark' });
+ await page.goto('/', { waitUntil: 'load' });
+ await zeppelinHelper.waitForZeppelinReady();
+ await expect(themePage.rootElement).toHaveClass(/dark/);
+ await expect(themePage.rootElement).toHaveAttribute('data-theme',
'dark');
+ await themePage.assertSystemTheme(); // Robot icon for system theme
+ });
+ });
+});
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index c5ceec8442..652cedc53f 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -76,7 +76,8 @@ export const PAGES = {
PAGE_HEADER: 'src/app/share/page-header/page-header.component',
RESIZE_HANDLE: 'src/app/share/resize-handle/resize-handle.component',
SHORTCUT: 'src/app/share/shortcut/shortcut.component',
- SPIN: 'src/app/share/spin/spin.component'
+ SPIN: 'src/app/share/spin/spin.component',
+ THEME_TOGGLE: 'src/app/share/theme-toggle/theme-toggle.component'
},
// Visualizations
diff --git a/zeppelin-web-angular/src/app/app.component.ts
b/zeppelin-web-angular/src/app/app.component.ts
index dc9fcb3afb..c5044b6a76 100644
--- a/zeppelin-web-angular/src/app/app.component.ts
+++ b/zeppelin-web-angular/src/app/app.component.ts
@@ -10,18 +10,18 @@
* limitations under the License.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { filter, map } from 'rxjs/operators';
-import { TicketService } from '@zeppelin/services';
+import { ThemeService, TicketService } from '@zeppelin/services';
@Component({
selector: 'zeppelin-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
-export class AppComponent {
+export class AppComponent implements OnInit {
logout$ = this.ticketService.logout$;
loading$ = this.router.events.pipe(
filter(data => data instanceof NavigationEnd || data instanceof
NavigationStart),
@@ -35,5 +35,9 @@ export class AppComponent {
})
);
- constructor(private router: Router, private ticketService: TicketService) {}
+ constructor(private router: Router, private ticketService: TicketService,
private themeService: ThemeService) {}
+
+ ngOnInit(): void {
+ this.themeService.updateMonacoTheme();
+ }
}
diff --git a/zeppelin-web-angular/src/app/languages/load.ts
b/zeppelin-web-angular/src/app/languages/load.ts
index 64c617acd6..f945f608e1 100644
--- a/zeppelin-web-angular/src/app/languages/load.ts
+++ b/zeppelin-web-angular/src/app/languages/load.ts
@@ -14,15 +14,9 @@ import { editor, languages } from 'monaco-editor';
import { conf as ScalaConf, language as ScalaLanguage } from './scala';
export const loadMonacoBefore = () => {
- editor.defineTheme('zeppelin-theme', {
- base: 'vs',
- inherit: true,
- rules: [],
- colors: {
- 'editor.lineHighlightBackground': '#0000FF10'
- }
- });
- editor.setTheme('zeppelin-theme');
+ const savedTheme = localStorage.getItem('zeppelin-theme') || 'light';
+ const monacoTheme = savedTheme === 'dark' ? 'vs-dark' : 'vs';
+ editor.setTheme(monacoTheme);
languages.register({ id: 'scala' });
languages.setMonarchTokensProvider('scala', ScalaLanguage);
languages.setLanguageConfiguration('scala', ScalaConf);
diff --git a/zeppelin-web-angular/src/app/pages/login/login.component.less
b/zeppelin-web-angular/src/app/pages/login/login.component.less
index 7aac210300..c4e89d0211 100644
--- a/zeppelin-web-angular/src/app/pages/login/login.component.less
+++ b/zeppelin-web-angular/src/app/pages/login/login.component.less
@@ -25,7 +25,6 @@
left: 0;
width: 100%;
height: 100%;
- background-image: url("../../../assets/images/bg.jpg");
background-size: cover;
filter: blur(4px);
background-repeat: no-repeat;
@@ -67,3 +66,19 @@
}
}
});
+
+:host-context(.light) {
+ .content {
+ &:after {
+ background-image: url("../../../assets/images/bg.jpg");
+ }
+ }
+}
+
+:host-context(.dark) {
+ .content {
+ &:after {
+ background-image: url("../../../assets/images/bg-dark.png");
+ }
+ }
+}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.less
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.less
index ea488728b3..0132935731 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.less
+++
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.less
@@ -29,11 +29,6 @@
}
}
- .editable-tag {
- background: @white;
- border-style: dashed;
- }
-
.content {
padding: @card-padding-base / 2;
@@ -43,3 +38,22 @@
}
}
});
+
+:host-context(.light) {
+ background: #f0f0f0;
+
+ .editable-tag {
+ background: #fff;
+ border-style: dashed;
+ }
+}
+
+:host-context(.dark) {
+ background: #141414;
+
+ .editable-tag {
+ background: #1f1f1f;
+ border-style: dashed;
+ border-color: #434343;
+ }
+}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less
b/zeppelin-web-angular/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less
index 0ca115b8fe..e76e332e5c 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less
@@ -18,7 +18,7 @@
text-align: center;
color: @primary-color;
font-weight: 500;
- position: relative;;
+ position: relative;
.inner {
position: absolute;
@@ -29,7 +29,6 @@
z-index: 10;
display: none;
line-height: 30px;
- background: @blue-1;
border: 1px solid @border-color-split;
box-shadow: @btn-shadow;
padding: 0 12px;
@@ -47,3 +46,15 @@
}
}
});
+
+:host-context(.light) {
+ .add-paragraph .inner {
+ background: @blue-1;
+ }
+}
+
+:host-context(.dark) {
+ .add-paragraph .inner {
+ background: #141414;
+ }
+}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index a9e7d6d968..33e8e9385d 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -43,6 +43,7 @@ import {
NoteStatusService,
NoteVarShareService,
SecurityService,
+ ThemeService,
TicketService
} from '@zeppelin/services';
@@ -105,6 +106,7 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
});
}
this.titleService.setTitle(this.note?.name + ' - Zeppelin');
+ this.themeService.updateMonacoTheme();
this.cdr.markForCheck();
}
}
@@ -408,7 +410,8 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
private ticketService: TicketService,
private securityService: SecurityService,
private router: Router,
- private titleService: Title
+ private titleService: Title,
+ private themeService: ThemeService
) {
super(messageService);
}
diff --git a/zeppelin-web-angular/src/app/services/public-api.ts
b/zeppelin-web-angular/src/app/services/public-api.ts
index 48b5f40849..a554dc9cf4 100644
--- a/zeppelin-web-angular/src/app/services/public-api.ts
+++ b/zeppelin-web-angular/src/app/services/public-api.ts
@@ -31,4 +31,5 @@ export * from './runtime-compiler.service';
export * from './save-as.service';
export * from './security.service';
export * from './shortcut.service';
+export * from './theme.service';
export * from './ticket.service';
diff --git a/zeppelin-web-angular/src/app/services/theme.service.ts
b/zeppelin-web-angular/src/app/services/theme.service.ts
new file mode 100644
index 0000000000..ae147412cd
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/theme.service.ts
@@ -0,0 +1,161 @@
+/*
+ * 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 { Injectable, OnDestroy } from '@angular/core';
+import { combineLatest, fromEvent, BehaviorSubject, Observable, Subscription }
from 'rxjs';
+import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
+
+export type ThemeMode = 'light' | 'dark' | 'system';
+
+const THEME_STORAGE_KEY = 'zeppelin-theme';
+const MONACO_THEMES = {
+ light: 'vs',
+ dark: 'vs-dark'
+} as const;
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ThemeService implements OnDestroy {
+ private themeSubject: BehaviorSubject<ThemeMode>;
+ private effectiveThemeSubject: BehaviorSubject<'light' | 'dark'>;
+ private systemStartedWith: 'light' | 'dark' | null = null;
+ private subscriptions = new Subscription();
+
+ public theme$: Observable<ThemeMode>;
+ public effectiveTheme$: Observable<'light' | 'dark'>;
+
+ ngOnDestroy() {
+ this.subscriptions.unsubscribe();
+ this.themeSubject.complete();
+ this.effectiveThemeSubject.complete();
+ }
+
+ constructor() {
+ const initialTheme = this.detectInitialTheme();
+ this.themeSubject = new BehaviorSubject<ThemeMode>(initialTheme);
+ this.theme$ = this.themeSubject.asObservable();
+
+ // Create observable for system theme changes
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const systemTheme$ = fromEvent<MediaQueryListEvent>(mediaQuery,
'change').pipe(
+ map(e => (e.matches ? ('dark' as const) : ('light' as const))),
+ startWith(mediaQuery.matches ? ('dark' as const) : ('light' as const)),
+ distinctUntilChanged()
+ );
+
+ // Calculate initial effective theme
+ const initialEffectiveTheme = this.resolveEffectiveTheme(initialTheme,
mediaQuery.matches);
+ this.effectiveThemeSubject = new BehaviorSubject<'light' |
'dark'>(initialEffectiveTheme);
+ this.effectiveTheme$ = this.effectiveThemeSubject.asObservable();
+
+ // Reactively update effective theme when either theme or system theme
changes
+ const subscription = combineLatest([this.theme$, systemTheme$])
+ .pipe(
+ map(([theme, systemTheme]) => (theme === 'system' ? systemTheme :
theme)),
+ distinctUntilChanged()
+ )
+ .subscribe(effectiveTheme => {
+ this.effectiveThemeSubject.next(effectiveTheme);
+ this.applyTheme(effectiveTheme);
+ });
+
+ this.subscriptions.add(subscription);
+ }
+
+ private detectInitialTheme(): ThemeMode {
+ try {
+ const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
+ if (savedTheme && this.isValidTheme(savedTheme)) {
+ return savedTheme;
+ }
+ return 'system';
+ } catch {
+ return 'system';
+ }
+ }
+
+ private isValidTheme(theme: string): theme is ThemeMode {
+ return theme === 'light' || theme === 'dark' || theme === 'system';
+ }
+
+ private resolveEffectiveTheme(theme: ThemeMode, isDarkMode: boolean):
'light' | 'dark' {
+ return theme === 'system' ? (isDarkMode ? 'dark' : 'light') : theme;
+ }
+
+ getCurrentTheme(): ThemeMode {
+ return this.themeSubject.value;
+ }
+
+ private getEffectiveTheme(): 'light' | 'dark' {
+ return this.effectiveThemeSubject.value;
+ }
+
+ private setTheme(theme: ThemeMode, save: boolean = true) {
+ if (this.themeSubject.value === theme) {
+ return;
+ }
+
+ this.themeSubject.next(theme);
+
+ if (save) {
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
+ }
+ }
+
+ toggleTheme() {
+ const currentTheme = this.getCurrentTheme();
+ let nextTheme: ThemeMode;
+
+ if (currentTheme === 'light') {
+ nextTheme = 'dark';
+ } else if (currentTheme === 'dark') {
+ nextTheme = 'system';
+ } else {
+ nextTheme = 'light';
+ }
+
+ this.setTheme(nextTheme);
+ }
+
+ private applyTheme(effectiveTheme: 'light' | 'dark') {
+ const html = document.documentElement;
+ const body = document.body;
+
+ [html, body].forEach(el => {
+ el.classList.toggle('dark', effectiveTheme === 'dark');
+ el.classList.toggle('light', effectiveTheme === 'light');
+ el.setAttribute('data-theme', effectiveTheme);
+ });
+
+ html.style.setProperty('color-scheme', effectiveTheme);
+
+ this.updateMonacoTheme();
+ }
+
+ updateMonacoTheme() {
+ if (!monaco?.editor) {
+ return;
+ }
+
+ const effectiveTheme = this.getEffectiveTheme();
+
+ try {
+ // Fix editor not applying dark mode on first load when theme is set to
"system"
+ requestAnimationFrame(() => {
+ monaco.editor.setTheme(MONACO_THEMES[effectiveTheme]);
+ });
+ } catch (error) {
+ console.error('Monaco theme setting failed:', error);
+ }
+ }
+}
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.html
b/zeppelin-web-angular/src/app/share/header/header.component.html
index f22da904a1..f0dd608aff 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.html
+++ b/zeppelin-web-angular/src/app/share/header/header.component.html
@@ -81,4 +81,5 @@
<input type="text" nz-input placeholder="Search"
(keyup.enter)="onSearch()" [(ngModel)]="queryStr" />
</nz-input-group>
</div>
+ <zeppelin-theme-toggle class="theme-toggle"></zeppelin-theme-toggle>
</div>
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.less
b/zeppelin-web-angular/src/app/share/header/header.component.less
index e4754b15ec..73865211e3 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.less
+++ b/zeppelin-web-angular/src/app/share/header/header.component.less
@@ -75,6 +75,11 @@
width: 300px;
margin-right: 24px;
}
+ .theme-toggle {
+ height: 100%;
+ display: flex;
+ float: right;
+ }
.user {
float: right;
diff --git a/zeppelin-web-angular/src/app/share/share.module.ts
b/zeppelin-web-angular/src/app/share/share.module.ts
index f514a950eb..d60f60844f 100644
--- a/zeppelin-web-angular/src/app/share/share.module.ts
+++ b/zeppelin-web-angular/src/app/share/share.module.ts
@@ -54,6 +54,7 @@ import { ResizeHandleComponent } from './resize-handle';
import { RunScriptsDirective } from './run-scripts/run-scripts.directive';
import { ShortcutComponent } from './shortcut/shortcut.component';
import { SpinComponent } from './spin/spin.component';
+import { ThemeToggleComponent } from './theme-toggle/theme-toggle.component';
const MODAL_LIST = [
AboutZeppelinComponent,
@@ -69,6 +70,7 @@ const EXPORT_LIST = [
NoteTocComponent,
PageHeaderComponent,
SpinComponent,
+ ThemeToggleComponent,
ResizeHandleComponent
];
const PIPES = [HumanizeBytesPipe];
diff --git
a/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.html
b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.html
new file mode 100644
index 0000000000..69a97fa813
--- /dev/null
+++
b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.html
@@ -0,0 +1,19 @@
+<!--
+ ~ 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.
+ -->
+
+<div class="theme-toggle-container">
+ <button nz-button nzType="link" nzSize="small" class="theme-toggle-button"
(click)="toggleTheme()">
+ <span class="material-symbols-outlined theme-text">
+ {{ themeIconName }}
+ </span>
+ </button>
+</div>
diff --git
a/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.less
b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.less
new file mode 100644
index 0000000000..32a24b0f50
--- /dev/null
+++
b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.less
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+.theme-toggle-container {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ justify-content: center;
+
+ .theme-toggle-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: none;
+ border: 1px solid #d9d9d9;
+ position: relative;
+ overflow: hidden;
+ cursor: pointer;
+ height: 31px;
+ width: 31px;
+
+ &:hover {
+ background-color: #f5f5f5;
+ border-color: #1890ff;
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+
+ .theme-icon {
+ font-size: 20px;
+ transition: none;
+ }
+
+ .theme-text {
+ font-size: 18px;
+ }
+
+ &:hover .theme-icon {
+ transform: none;
+ }
+ }
+}
+
+:host-context(.dark) .theme-toggle-container {
+ .theme-toggle-button {
+ color: #fff;
+ border-color: #434343;
+ background-color: #262626;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ border-color: #1890ff;
+ }
+
+ &:focus {
+ background-color: rgba(255, 255, 255, 0.08);
+ border-color: #1890ff;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+ }
+}
diff --git
a/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.ts
b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.ts
new file mode 100644
index 0000000000..35991d05e2
--- /dev/null
+++ b/zeppelin-web-angular/src/app/share/theme-toggle/theme-toggle.component.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy,
OnInit } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+import { ThemeMode, ThemeService } from '../../services/theme.service';
+
+@Component({
+ selector: 'zeppelin-theme-toggle',
+ templateUrl: './theme-toggle.component.html',
+ styleUrls: ['./theme-toggle.component.less'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ThemeToggleComponent implements OnInit, OnDestroy {
+ private destroy$ = new Subject();
+ currentTheme: ThemeMode = 'light';
+ isDarkMode = false;
+
+ constructor(private themeService: ThemeService, private cdr:
ChangeDetectorRef) {}
+
+ ngOnInit() {
+ this.currentTheme = this.themeService.getCurrentTheme();
+ this.isDarkMode = this.currentTheme === 'dark';
+
+ this.themeService.theme$.pipe(takeUntil(this.destroy$)).subscribe(theme =>
{
+ if (this.currentTheme !== theme) {
+ this.currentTheme = theme;
+ this.isDarkMode = theme === 'dark';
+ this.cdr.markForCheck();
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ toggleTheme() {
+ this.themeService.toggleTheme();
+ }
+
+ get themeIconName(): 'light_mode' | 'dark_mode' | 'smart_toy' {
+ if (this.currentTheme === 'light') {
+ return 'light_mode';
+ }
+ if (this.currentTheme === 'dark') {
+ return 'dark_mode';
+ }
+ return 'smart_toy';
+ }
+}
diff --git
a/zeppelin-web-angular/src/assets/fonts/MaterialSymbolsOutlined.woff2
b/zeppelin-web-angular/src/assets/fonts/MaterialSymbolsOutlined.woff2
new file mode 100644
index 0000000000..1c71ab04fb
Binary files /dev/null and
b/zeppelin-web-angular/src/assets/fonts/MaterialSymbolsOutlined.woff2 differ
diff --git
a/zeppelin-web-angular/src/assets/fonts/material-symbols-outlined.css
b/zeppelin-web-angular/src/assets/fonts/material-symbols-outlined.css
new file mode 100644
index 0000000000..4b148cd0bc
--- /dev/null
+++ b/zeppelin-web-angular/src/assets/fonts/material-symbols-outlined.css
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+@font-face {
+ font-family: 'Material Symbols Outlined';
+ font-style: normal;
+ font-weight: 100 700;
+ font-display: block;
+ src: url('./MaterialSymbolsOutlined.woff2') format('woff2');
+}
+
+.material-symbols-outlined {
+ font-family: 'Material Symbols Outlined';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 24px;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-feature-settings: 'liga';
+ font-feature-settings: 'liga';
+ -webkit-font-smoothing: antialiased;
+}
diff --git a/zeppelin-web-angular/src/assets/images/bg-dark.png
b/zeppelin-web-angular/src/assets/images/bg-dark.png
new file mode 100644
index 0000000000..53e428ae8a
Binary files /dev/null and b/zeppelin-web-angular/src/assets/images/bg-dark.png
differ
diff --git a/zeppelin-web-angular/src/index.html
b/zeppelin-web-angular/src/index.html
index 0e2b2a1604..3a9ee02fbc 100644
--- a/zeppelin-web-angular/src/index.html
+++ b/zeppelin-web-angular/src/index.html
@@ -16,7 +16,21 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="./assets/images/zeppelin.png" type="image/x-icon" />
+ <link rel="stylesheet" href="./assets/fonts/material-symbols-outlined.css"
/>
<title>Zeppelin</title>
+ <script>
+ // Prevent FOUC (Flash of Unstyled Content)
+ (function() {
+ const savedTheme = localStorage.getItem('zeppelin-theme');
+ const systemPrefersDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
+ const isDark =
+ savedTheme === 'dark' || (savedTheme === 'system' &&
systemPrefersDark) || (!savedTheme && systemPrefersDark);
+
+ if (isDark) {
+ document.documentElement.classList.add('dark');
+ }
+ })();
+ </script>
</head>
<body>
<zeppelin-root>
diff --git a/zeppelin-web-angular/src/styles/spin.less
b/zeppelin-web-angular/src/styles/spin.less
index ec8c0a5a88..b0957c3813 100644
--- a/zeppelin-web-angular/src/styles/spin.less
+++ b/zeppelin-web-angular/src/styles/spin.less
@@ -31,6 +31,10 @@
filter: blur(4px);
background-repeat: no-repeat;
background-position: center;
+
+ html.dark & {
+ background-image: url("../assets/images/bg-dark.png");
+ }
}
&.transparent {
@@ -40,6 +44,10 @@
}
& > div {
+ html.dark & {
+ background: unset;
+ }
+
background: rgba(255, 255, 255, 0.5);
text-align: center;
position: absolute;
diff --git a/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css
b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css
new file mode 100644
index 0000000000..6b762b2315
--- /dev/null
+++ b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css
@@ -0,0 +1,363 @@
+/*
+ * 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.
+ */
+
+/* Reset built-in transition */
+.ant-btn,
+.ant-input,
+.ant-input-affix-wrapper,
+.ant-input-group,
+.ant-input-group-addon,
+.ant-input-prefix,
+.ant-input-suffix,
+.ant-select,
+.ant-dropdown,
+.ant-menu,
+.ant-card,
+.ant-table,
+.ant-modal,
+.ant-drawer,
+.ant-tooltip,
+.ant-popover,
+.ant-notification,
+.ant-message,
+.ant-switch,
+.ant-radio,
+.ant-checkbox,
+.ant-tabs,
+.ant-collapse,
+.ant-slider,
+.ant-progress,
+.ant-badge,
+.ant-avatar,
+.ant-tag,
+.ant-alert,
+.ant-spin,
+.ant-pagination,
+.ant-breadcrumb,
+.ant-steps,
+.ant-form,
+.ant-upload,
+.ant-tree,
+.ant-list,
+.ant-calendar,
+.ant-date-picker,
+.ant-time-picker,
+.ant-cascader,
+.ant-transfer,
+.ant-rate,
+.ant-affix,
+.ant-anchor,
+.ant-back-top,
+.ant-divider,
+.ant-layout,
+.ant-grid,
+.ant-space,
+.search {
+ transition: none !important;
+ animation: none !important;
+}
+
+.ant-btn *,
+.ant-input *,
+.ant-input-affix-wrapper *,
+.ant-input-group *,
+.ant-input-group-addon *,
+.ant-input-prefix *,
+.ant-input-suffix *,
+.ant-select *,
+.ant-dropdown *,
+.ant-menu *,
+.ant-card *,
+.ant-table *,
+.ant-modal *,
+.ant-drawer *,
+.ant-tooltip *,
+.ant-popover *,
+.ant-notification *,
+.ant-message *,
+.ant-switch *,
+.ant-radio *,
+.ant-checkbox *,
+.ant-tabs *,
+.ant-collapse *,
+.ant-slider *,
+.ant-progress *,
+.ant-badge *,
+.ant-avatar *,
+.ant-tag *,
+.ant-alert *,
+.ant-spin *,
+.ant-pagination *,
+.ant-breadcrumb *,
+.ant-steps *,
+.ant-form *,
+.ant-upload *,
+.ant-tree *,
+.ant-list *,
+.ant-calendar *,
+.ant-date-picker *,
+.ant-time-picker *,
+.ant-cascader *,
+.ant-transfer *,
+.ant-rate *,
+.ant-affix *,
+.ant-anchor *,
+.ant-back-top *,
+.ant-divider *,
+.ant-layout *,
+.ant-grid *,
+.ant-space *,
+.search * {
+ transition: none !important;
+ animation: none !important;
+}
+
+/* Special handling for search area */
+.search,
+.search .ant-input-affix-wrapper,
+.search .ant-input,
+.search input {
+ transition: none !important;
+ animation: none !important;
+}
+
+.search:hover,
+.search:focus,
+.search:active,
+.search .ant-input-affix-wrapper:hover,
+.search .ant-input-affix-wrapper:focus,
+.search .ant-input-affix-wrapper:active,
+.search .ant-input:hover,
+.search .ant-input:focus,
+.search .ant-input:active,
+.search input:hover,
+.search input:focus,
+.search input:active {
+ transition: none !important;
+ animation: none !important;
+}
+
+/* Prevent dark mode dropdown menu flickering */
+html.dark .ant-dropdown-menu,
+html.dark .ant-dropdown-menu-item,
+html.dark .ant-dropdown-menu-submenu-title,
+html.dark .ant-menu-item,
+html.dark .ant-menu-submenu-title {
+ background-color: #1f1f1f !important;
+ color: rgba(255, 255, 255, 0.85) !important;
+ transition: none !important;
+ animation: none !important;
+}
+
+html.dark .ant-dropdown-menu-item:hover,
+html.dark .ant-dropdown-menu-submenu-title:hover,
+html.dark .ant-menu-item:hover,
+html.dark .ant-menu-submenu-title:hover {
+ background-color: #262626 !important;
+ color: rgba(255, 255, 255, 0.95) !important;
+}
+
+html.dark .ant-dropdown-menu-item-selected,
+html.dark .ant-dropdown-menu-submenu-title-selected,
+html.dark .ant-dropdown-menu-item-selected > a,
+html.dark .ant-dropdown-menu-submenu-title-selected > a,
+html.dark .ant-menu-item-selected {
+ background-color: #262626 !important;
+ color: #1890ff !important;
+}
+
+html.dark .ant-menu-item-divider,
+html.dark .ant-dropdown-menu-item-divider {
+ background-color: #434343 !important;
+}
+
+/* Dark mode alert background color adjustments */
+html.dark .ant-alert-info {
+ background-color: #111b26 !important;
+ border-color: #153450 !important;
+}
+
+html.dark .ant-alert-success {
+ background-color: #162312 !important;
+ border-color: #274916 !important;
+}
+
+html.dark .ant-alert-warning {
+ background-color: #2b1d11 !important;
+ border-color: #594214 !important;
+}
+
+html.dark .ant-alert-error {
+ background-color: #2a1215 !important;
+ border-color: #58181c !important;
+}
+
+/* Dark mode upload drag area background color adjustments */
+html.dark .ant-upload.ant-upload-drag {
+ background-color: #1f1f1f !important;
+ border-color: #434343 !important;
+}
+
+html.dark .ant-upload.ant-upload-drag:hover {
+ border-color: #1890ff !important;
+}
+
+html.dark .ant-upload.ant-upload-drag.ant-upload-drag-hover {
+ border-color: #1890ff !important;
+}
+
+html.dark .ant-upload.ant-upload-drag p.ant-upload-text,
+html.dark .ant-upload.ant-upload-drag p.ant-upload-hint {
+ color: rgba(255, 255, 255, 0.85) !important;
+}
+
+/* Tree node hover color adjustments */
+html.dark .ant-tree .ant-tree-node-content-wrapper:hover {
+ background-color: rgba(255, 255, 255, 0.06) !important;
+}
+
+/* Tree selected node background color adjustments */
+html.dark .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected {
+ background-color: rgba(24, 144, 255, 0.15) !important;
+}
+
+/* Monaco editor background color matching */
+html.dark .ant-code-editor {
+ background-color: #1e1e1e !important;
+}
+
+/* Markdown code tag background color adjustments */
+html.dark .markdown-body code {
+ background-color: #2d2d2d !important;
+ color: rgba(255, 255, 255, 0.85) !important;
+}
+
+html.dark .markdown-body pre {
+ background-color: #1e1e1e !important;
+ color: rgba(255, 255, 255, 0.85) !important;
+ border: 1px solid #434343 !important;
+}
+
+html.dark .markdown-body pre code {
+ background-color: transparent !important;
+ color: inherit !important;
+}
+
+/* Table row hover color fixes */
+html.dark .ant-table-tbody > tr.ant-table-row:hover > td {
+ background-color: #262626 !important;
+}
+
+/* Table placeholder row hover color fix */
+html.dark .ant-table-tbody > tr.ant-table-placeholder:hover > td {
+ background-color: transparent !important;
+}
+
+/* Input field dark mode styling */
+html.dark .ant-input,
+html.dark .ant-input-affix-wrapper,
+html.dark .ant-input-affix-wrapper > input.ant-input {
+ background-color: #262626 !important;
+ border-color: #434343 !important;
+ color: rgba(255, 255, 255, 0.85) !important;
+}
+
+html.dark .ant-input::placeholder,
+html.dark .ant-input-affix-wrapper > input.ant-input::placeholder {
+ color: rgba(255, 255, 255, 0.45) !important;
+}
+
+html.dark .ant-input:hover,
+html.dark .ant-input-affix-wrapper:hover,
+html.dark .ant-input-affix-wrapper:hover > input.ant-input {
+ border-color: #177ddc !important;
+ background-color: #262626 !important;
+}
+
+html.dark .ant-input:focus,
+html.dark .ant-input-affix-wrapper-focused,
+html.dark .ant-input-affix-wrapper:focus,
+html.dark .ant-input-affix-wrapper-focused > input.ant-input,
+html.dark .ant-input-affix-wrapper:focus > input.ant-input {
+ border-color: #177ddc !important;
+ background-color: #262626 !important;
+ box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2) !important;
+}
+
+html.dark .ant-input-prefix,
+html.dark .ant-input-suffix {
+ color: rgba(255, 255, 255, 0.65) !important;
+}
+
+/* Highlight.js (hljs) dark mode styling */
+html.dark .hljs,
+html.dark pre code.hljs {
+ background-color: #1e1e1e !important;
+ color: rgba(255, 255, 255, 0.85) !important;
+}
+
+html.dark .hljs-comment,
+html.dark .hljs-quote {
+ color: #6a9955 !important;
+}
+
+html.dark .hljs-keyword,
+html.dark .hljs-selector-tag,
+html.dark .hljs-subst {
+ color: #569cd6 !important;
+}
+
+html.dark .hljs-string,
+html.dark .hljs-attr {
+ color: #ce9178 !important;
+}
+
+html.dark .hljs-number,
+html.dark .hljs-literal,
+html.dark .hljs-variable,
+html.dark .hljs-template-variable,
+html.dark .hljs-tag .hljs-attr {
+ color: #b5cea8 !important;
+}
+
+html.dark .hljs-built_in,
+html.dark .hljs-builtin-name {
+ color: #dcdcaa !important;
+}
+
+html.dark .hljs-type,
+html.dark .hljs-class .hljs-title {
+ color: #4ec9b0 !important;
+}
+
+html.dark .hljs-function .hljs-title,
+html.dark .hljs-title.function_ {
+ color: #dcdcaa !important;
+}
+
+html.dark .hljs-meta {
+ color: #9cdcfe !important;
+}
+
+/* Scala specific hljs styling */
+html.dark .hljs-scala .hljs-keyword {
+ color: #569cd6 !important;
+}
+
+html.dark .hljs-scala .hljs-class,
+html.dark .hljs-scala .hljs-type {
+ color: #4ec9b0 !important;
+}
+
+html.dark .hljs-scala .hljs-function {
+ color: #dcdcaa !important;
+}
diff --git a/zeppelin-web-angular/src/styles/theme/dark/theme-dark.less
b/zeppelin-web-angular/src/styles/theme/dark/theme-dark.less
index 26e9186728..64ddf2f9cd 100644
--- a/zeppelin-web-angular/src/styles/theme/dark/theme-dark.less
+++ b/zeppelin-web-angular/src/styles/theme/dark/theme-dark.less
@@ -47,20 +47,20 @@
// ---
// Background color for `<body>`
-@body-background: #fff;
+@body-background: #141414;
// Base background color for most components
-@component-background: #fff;
+@component-background: #1f1f1f;
@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei',
'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe
UI Emoji', 'Segoe UI Symbol';
@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
-@text-color: fade(@black, 65%);
-@text-color-secondary: fade(@black, 45%);
+@text-color: fade(@white, 85%);
+@text-color-secondary: fade(@white, 65%);
@text-color-warning: @gold-7;
@text-color-danger: @red-7;
-@text-color-inverse: @white;
-@icon-color: inherit;
-@icon-color-hover: fade(@black, 75%);
-@heading-color: fade(#000, 85%);
+@text-color-inverse: @black;
+@icon-color: fade(@white, 65%);
+@icon-color-hover: fade(@white, 85%);
+@heading-color: fade(@white, 95%);
@heading-color-dark: fade(@white, 100%);
@text-color-dark: fade(@white, 85%);
@text-color-secondary-dark: fade(@white, 65%);
@@ -119,9 +119,9 @@
@ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
// Border color
-@border-color-base: hsv(0, 0, 85%); // base border outline a component
-@border-color-split: hsv(0, 0, 91%); // split border inside a component
-@border-color-inverse: @white;
+@border-color-base: #434343; // base border outline a component
+@border-color-split: #303030; // split border inside a component
+@border-color-inverse: @black;
@border-width-base: 1px; // width of the border for a component
@border-style-base: solid; // style of a components border
@@ -130,16 +130,16 @@
@outline-width: 2px;
@outline-color: @primary-color;
-@background-color-light: hsv(0, 0, 98%); // background of header and selected
item
-@background-color-base: hsv(0, 0, 96%); // Default grey background color
+@background-color-light: #262626; // background of header and selected item
+@background-color-base: #1f1f1f; // Default grey background color
// Disabled states
-@disabled-color: fade(#000, 25%);
+@disabled-color: fade(@white, 25%);
@disabled-bg: @background-color-base;
-@disabled-color-dark: fade(#fff, 35%);
+@disabled-color-dark: fade(@white, 35%);
// Shadow
-@shadow-color: rgba(0, 0, 0, 0.15);
+@shadow-color: rgba(0, 0, 0, 0.45);
@shadow-color-inverse: @component-background;
@box-shadow-base: @shadow-1-down;
@shadow-1-up: 0 -2px 8px @shadow-color;
@@ -158,11 +158,11 @@
@btn-primary-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
@btn-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
-@btn-primary-color: #fff;
+@btn-primary-color: @white;
@btn-primary-bg: @primary-color;
@btn-default-color: @text-color;
-@btn-default-bg: #fff;
+@btn-default-bg: @component-background;
@btn-default-border: @border-color-base;
@btn-danger-color: @error-color;
@@ -196,7 +196,7 @@
// Checkbox
@checkbox-size: 16px;
@checkbox-color: @primary-color;
-@checkbox-check-color: #fff;
+@checkbox-check-color: @white;
@checkbox-border-width: @border-width-base;
// Descriptions
@@ -253,7 +253,7 @@
@grid-gutter-width: 0;
// Layout
-@layout-body-background: #f0f2f5;
+@layout-body-background: @body-background;
@layout-header-background: #001529;
@layout-footer-background: @layout-body-background;
@layout-header-height: 64px;
@@ -262,12 +262,12 @@
@layout-sider-background: @layout-header-background;
@layout-trigger-height: 48px;
@layout-trigger-background: #002140;
-@layout-trigger-color: #fff;
+@layout-trigger-color: @white;
@layout-zero-trigger-width: 36px;
@layout-zero-trigger-height: 42px;
// Layout light theme
-@layout-sider-background-light: #fff;
-@layout-trigger-background-light: #fff;
+@layout-sider-background-light: @white;
+@layout-trigger-background-light: @white;
@layout-trigger-color-light: @text-color;
// z-index list, order by `z-index`
@@ -312,11 +312,11 @@
@input-padding-vertical-base: 4px;
@input-padding-vertical-sm: 1px;
@input-padding-vertical-lg: 6px;
-@input-placeholder-color: hsv(0, 0, 75%);
+@input-placeholder-color: fade(@white, 45%);
@input-color: @text-color;
@input-border-color: @border-color-base;
-@input-bg: #fff;
-@input-number-handler-active-bg: #f4f4f4;
+@input-bg: @component-background;
+@input-number-handler-active-bg: @background-color-light;
@input-number-handler-hover-bg: @primary-5;
@input-number-handler-bg: @component-background;
@input-number-handler-border-color: @border-color-base;
@@ -342,7 +342,7 @@
// Tooltip max width
@tooltip-max-width: 250px;
// Tooltip text color
-@tooltip-color: #fff;
+@tooltip-color: @white;
// Tooltip background color
@tooltip-bg: rgba(0, 0, 0, 0.75);
// Tooltip arrow width
@@ -355,7 +355,7 @@
// Popover
// ---
// Popover body background color
-@popover-bg: #fff;
+@popover-bg: @component-background;
// Popover text color
@popover-color: @text-color;
// Popover maximum width
@@ -376,7 +376,7 @@
@modal-header-bg: @component-background;
@modal-footer-bg: transparent;
@modal-footer-border-color-split: @border-color-split;
-@modal-mask-bg: fade(@black, 45%);
+@modal-mask-bg: fade(@black, 65%);
// Progress
// --
@@ -411,9 +411,9 @@
// dark theme
@menu-dark-color: @text-color-secondary-dark;
@menu-dark-bg: @layout-header-background;
-@menu-dark-arrow-color: #fff;
+@menu-dark-arrow-color: @white;
@menu-dark-submenu-bg: #000c17;
-@menu-dark-highlight-color: #fff;
+@menu-dark-highlight-color: @white;
@menu-dark-item-active-bg: @primary-color;
@menu-dark-selected-item-icon-color: @white;
@menu-dark-selected-item-text-color: @white;
@@ -427,17 +427,17 @@
// Table
// --
-@table-header-bg: @background-color-light;
+@table-header-bg: @background-color-base;
@table-header-color: @heading-color;
@table-header-sort-bg: @background-color-base;
-@table-body-sort-bg: rgba(0, 0, 0, 0.01);
+@table-body-sort-bg: rgba(255, 255, 255, 0.01);
@table-row-hover-bg: @primary-1;
-@table-selected-row-bg: #fafafa;
-@table-expanded-row-bg: #fbfbfb;
+@table-selected-row-bg: @background-color-base;
+@table-expanded-row-bg: @background-color-base;
@table-padding-vertical: 16px;
@table-padding-horizontal: 16px;
@table-border-radius-base: @border-radius-base;
-@table-footer-bg: @background-color-light;
+@table-footer-bg: @background-color-base;
@table-footer-color: @heading-color;
// Tag
@@ -480,9 +480,9 @@
@card-inner-head-padding: 12px;
@card-padding-base: 24px;
@card-actions-background: @background-color-light;
-@card-skeleton-bg: #cfd8dc;
+@card-skeleton-bg: #434343;
@card-background: @component-background;
-@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
+@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
@card-radius: @border-radius-sm;
// Comment
@@ -490,9 +490,9 @@
@comment-padding-base: 16px 0;
@comment-nest-indent: 44px;
@comment-author-name-color: @text-color-secondary;
-@comment-author-time-color: #ccc;
+@comment-author-time-color: @text-color-secondary;
@comment-action-color: @text-color-secondary;
-@comment-action-hover-color: #595959;
+@comment-action-hover-color: @text-color;
// Tabs
// ---
@@ -517,7 +517,7 @@
// BackTop
// ---
-@back-top-color: #fff;
+@back-top-color: @white;
@back-top-bg: @text-color-secondary;
@back-top-hover-bg: @text-color;
@@ -529,8 +529,8 @@
@avatar-font-size-base: 18px;
@avatar-font-size-lg: 24px;
@avatar-font-size-sm: 14px;
-@avatar-bg: #ccc;
-@avatar-color: #fff;
+@avatar-bg: @text-color-secondary;
+@avatar-color: @white;
@avatar-border-radius: @border-radius-base;
// Switch
@@ -555,7 +555,7 @@
@page-header-padding: 24px;
@page-header-padding-vertical: 16px; @page-header-padding-vertical: 16px;
@page-header-padding-breadcrumb: 12px;
-@page-header-back-color: #000;
+@page-header-back-color: @black;
// Breadcrumb
// ---
@@ -589,9 +589,9 @@
// ---
@tree-title-height: 24px;
@tree-child-padding: 18px;
-@tree-directory-selected-color: #fff;
+@tree-directory-selected-color: @white;
@tree-directory-selected-bg: @primary-color;
-@tree-node-hover-bg: @item-hover-bg;
+@tree-node-hover-bg: @black;
@tree-node-selected-bg: @primary-2;
// Collapse
@@ -603,7 +603,7 @@
// Skeleton
// ---
-@skeleton-color: #f2f2f2;
+@skeleton-color: #434343;
// Transfer
// ---
diff --git a/zeppelin-web-angular/src/styles/theme/markdown.less
b/zeppelin-web-angular/src/styles/theme/markdown.less
index d9f26833d6..3ee3319e23 100644
--- a/zeppelin-web-angular/src/styles/theme/markdown.less
+++ b/zeppelin-web-angular/src/styles/theme/markdown.less
@@ -12,15 +12,15 @@
.markdown-body {
color: @text-color;
- font-size: 14px;
+ font-size: 14px !important;
line-height: 1.5;
h1 {
color: @heading-color;
- font-weight: 500;
+ font-weight: 500 !important;
margin: 0.6em 0 0.6em;
- font-family: Avenir, @font-family;
- font-size: 30px;
+ font-family: Avenir, @font-family !important;
+ font-size: 30px !important;
font-variant: tabular-nums;
line-height: 38px;
}
@@ -36,7 +36,7 @@
h5,
h6 {
color: @heading-color;
- font-family: Avenir, @font-family;
+ font-family: Avenir, @font-family !important;
font-variant: tabular-nums;
margin: 0.6em 0 0.6em;
font-weight: 500;
@@ -94,12 +94,10 @@
}
code {
- margin: 0 1px;
background: #f2f4f5;
padding: .2em .4em;
border-radius: 3px;
- font-size: .9em;
- border: 1px solid #eee;
+ font-size: .9em !important;
}
pre {
@@ -113,7 +111,7 @@
background: #f2f4f5;
margin: 0;
padding: 0;
- font-size: @font-size-base - 1px;
+ font-size: @font-size-base - 1px !important;
color: @text-color;
overflow: auto;
}