diff --git a/src/standalone/plugins/top-bar/assets/lightbulb-off.svg b/src/standalone/plugins/top-bar/assets/lightbulb-off.svg
new file mode 100644
index 00000000000..19b398fa6a7
--- /dev/null
+++ b/src/standalone/plugins/top-bar/assets/lightbulb-off.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/standalone/plugins/top-bar/assets/lightbulb.svg b/src/standalone/plugins/top-bar/assets/lightbulb.svg
new file mode 100644
index 00000000000..d9bfec1b070
--- /dev/null
+++ b/src/standalone/plugins/top-bar/assets/lightbulb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/standalone/plugins/top-bar/components/DarkMode.jsx b/src/standalone/plugins/top-bar/components/DarkMode.jsx
new file mode 100644
index 00000000000..cfe3f21a753
--- /dev/null
+++ b/src/standalone/plugins/top-bar/components/DarkMode.jsx
@@ -0,0 +1,27 @@
+/**
+ * @prettier
+ */
+import React, { useState, useCallback } from "react"
+import LightBulb from "../assets/lightbulb.svg"
+import LightBulbOff from "../assets/lightbulb-off.svg"
+
+const LightBulbIcon = () => {
+ const [isDarkMode, setIsDarkMode] = useState(() => window.matchMedia('(prefers-color-scheme: dark)').matches)
+
+ const toggleDarkMode = useCallback(() => {
+ document.documentElement.classList.toggle("dark")
+ setIsDarkMode((prevState) => !prevState) // Toggle the state
+ }, [])
+
+ return (
+
+ {!isDarkMode ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default LightBulbIcon
diff --git a/src/standalone/plugins/top-bar/components/TopBar.jsx b/src/standalone/plugins/top-bar/components/TopBar.jsx
index 1344d347939..16e512980a4 100644
--- a/src/standalone/plugins/top-bar/components/TopBar.jsx
+++ b/src/standalone/plugins/top-bar/components/TopBar.jsx
@@ -113,6 +113,7 @@ class TopBar extends React.Component {
const Button = getComponent("Button")
const Link = getComponent("Link")
const Logo = getComponent("Logo")
+ const DarkMode = getComponent("DarkMode")
let isLoading = specSelectors.loadingStatus() === "loading"
let isFailed = specSelectors.loadingStatus() === "failed"
@@ -164,6 +165,7 @@ class TopBar extends React.Component {
+
diff --git a/src/standalone/plugins/top-bar/index.js b/src/standalone/plugins/top-bar/index.js
index 03092fc692d..b46592bbe81 100644
--- a/src/standalone/plugins/top-bar/index.js
+++ b/src/standalone/plugins/top-bar/index.js
@@ -3,9 +3,10 @@
*/
import TopBar from "./components/TopBar"
import Logo from "./components/Logo"
+import DarkMode from "./components/DarkMode"
const TopBarPlugin = () => ({
- components: { Topbar: TopBar, Logo },
+ components: { Topbar: TopBar, Logo, DarkMode },
})
export default TopBarPlugin
diff --git a/src/style/_themeDark.scss b/src/style/_themeDark.scss
new file mode 100644
index 00000000000..5bdd93a6c96
--- /dev/null
+++ b/src/style/_themeDark.scss
@@ -0,0 +1,172 @@
+// Variables for consistent theme
+$text_color_1: #ffffffde;
+$text_color_2: #f1f1f1e5;
+$text_color_3: #f1f1f16e;
+
+$background_main: #141316;
+$background_topBar: #222129;
+$background_body: #0f0e14;
+$background_schemeCont: #16151a;
+
+$opblock_colors: (
+ post: (#0025142e, #09633a, #16231f99),
+ deprecated: (#1b161e, #594d69, #29242f),
+ put: (#a1661e1f, #fca13096, #412d24),
+ get: (#3f78b31f, #5a99d99c, #355d855e),
+ delete: (#77191933, #993434, #511c1ccc),
+);
+
+$models_colors: (
+ background: #1f2027,
+ border: #4b507175,
+ item_background: #2e2e35d9,
+ item_hover: #37373f,
+);
+
+html.dark {
+ background: $background_main;
+
+
+ .swagger-ui {
+ color: $text_color_2;
+ background: $background_main;
+
+ .expand-operation,
+ .authorization__btn svg,
+ .expand-operation svg,
+ .opblock-control-arrow svg {
+ fill: $text_color_2;
+ }
+
+ a.nostyle,
+ a.nostyle:visited,
+ .opblock-tag small {
+ color: $text_color_1;
+ }
+
+ .info {
+ h1, h2, h3, h4, h5, .title {
+ color: $text_color_1;
+ }
+ li, p, table {
+ color: $text_color_2;
+ }
+ .base-url {
+ color: $text_color_3;
+ }
+ }
+
+ .topbar {
+ background: $background_topBar;
+ .download-url-wrapper .download-url-button {
+ color: $text_color_2;
+ }
+ }
+
+ .scheme-container {
+ background: $background_schemeCont;
+ .schemes > .schemes-server-container > label {
+ color: $text_color_2;
+ }
+ }
+
+ table thead tr {
+ td, th {
+ color: $text_color_2;
+ }
+ }
+
+ // OPblock styles
+ @each $type, $colors in $opblock_colors {
+ .opblock-#{$type} {
+ background: nth($colors, 1);
+ border-color: nth($colors, 2);
+ .opblock-section-header {
+ background-color: nth($colors, 3);
+ }
+ .opblock-summary {
+ border-color: nth($colors, 2);
+ }
+ }
+ }
+
+ .opblock {
+ .opblock-section-header {
+ h4 {
+ color: $text_color_1;
+ }
+ label {
+ color: $text_color_3;
+ }
+ .btn {
+ color: $text_color_2;
+ border-color: $text_color_3;
+ box-shadow: none;
+ }
+ }
+
+ .model-box {
+ background: map-get($models_colors, background);
+ border-color: map-get($models_colors, border);
+ .model-title {
+ color: $text_color_1;
+ }
+ .model {
+ color: $text_color_2;
+ }
+ }
+
+ .markdown p,
+ .markdown pre,
+ .renderedMarkdown p,
+ .renderedMarkdown pre {
+ color: $text_color_2;
+ }
+
+ .opblock-summary-operation-id,
+ .opblock-summary-path,
+ .opblock-summary-path__deprecated,
+ .opblock-summary-description,
+ .description-wrapper,
+ .opblock-external-docs-wrapper,
+ .opblock-title_normal,
+ .parameter__name,
+ .parameter__type,
+ .parameter__in,
+ .response-col_status {
+ color: $text_color_3;
+ }
+ }
+
+ section.models {
+ background: map-get($models_colors, background);
+ border-color: map-get($models_colors, border);
+
+ h4 span,
+ .model-title {
+ color: $text_color_1;
+ }
+
+ .models-control svg {
+ fill: $text_color_2;
+ }
+
+ .model-container {
+ background: map-get($models_colors, item_background);
+ &:hover {
+ background: map-get($models_colors, item_hover);
+ }
+ }
+
+ .model {
+ color: $text_color_3;
+ }
+ }
+
+ .model-box-control:focus,
+ .models-control:focus,
+ .opblock-summary-control:focus {
+ outline: none;
+ }
+ }
+}
diff --git a/src/style/_topbar.scss b/src/style/_topbar.scss
index da3196030f1..48fac1673f4 100644
--- a/src/style/_topbar.scss
+++ b/src/style/_topbar.scss
@@ -1,7 +1,6 @@
.topbar
{
padding: 10px 0;
-
background-color: $topbar-background-color;
.topbar-wrapper
{
@@ -22,13 +21,10 @@
{
font-size: 1.5em;
font-weight: bold;
-
display: flex;
align-items: center;
flex: 1;
-
max-width: 300px;
-
text-decoration: none;
@include text_headline($topbar-link-font-color);
@@ -45,6 +41,8 @@
display: flex;
flex: 3;
justify-content: flex-end;
+ max-width: 600px;
+ margin-left: auto;
input[type=text]
{
@@ -110,4 +108,19 @@
width: 100%;
}
}
+
+ .dark-toggle {
+ margin-left: 10px;
+ opacity: 0.8;
+ transition: all .2s;
+ svg {
+ fill: #f1f1f1cb;
+ }
+ &:hover {
+ opacity: 1;
+ svg {
+ fill: #ffff0081;
+ }
+ }
+ }
}
diff --git a/src/style/main.scss b/src/style/main.scss
index 413786180c3..28e6d9f7e9a 100644
--- a/src/style/main.scss
+++ b/src/style/main.scss
@@ -21,3 +21,6 @@
@import '../core/plugins/json-schema-2020-12/components/all';
@import '../core/plugins/oas31/components/all';
}
+
+// Themes must live outside of the '.swagger-ui' class to target HTML parent.
+@import 'themeDark';
diff --git a/test/unit/components/darkMode.jsx b/test/unit/components/darkMode.jsx
new file mode 100644
index 00000000000..e890be8f716
--- /dev/null
+++ b/test/unit/components/darkMode.jsx
@@ -0,0 +1,32 @@
+import React from "react"
+import { mount } from "enzyme"
+import DarkMode from "../../../src/standalone/plugins/top-bar/components/DarkMode"
+
+// Mock SVG imports
+jest.mock("../../../src/assets/lightbulb.svg", () => () => LightBulb
)
+jest.mock("../../../src/assets/lightbulb-off.svg", () => () => LightBulbOff
)
+
+describe("LightBulbIcon Component", () => {
+ it("toggles the dark class on the html element and switches icons on click", () => {
+ // Mount the component
+ const wrapper = mount( )
+
+ // Access the root html element
+ const htmlElement = document.documentElement
+
+ // Ensure initial state no "dark" class
+ expect(htmlElement.classList.contains("dark")).toBe(false)
+
+ // Simulate the first click
+ wrapper.find(".dark-toggle").simulate("click")
+
+ // After the first click "dark" class is added
+ expect(htmlElement.classList.contains("dark")).toBe(true)
+
+ // Simulate the second click
+ wrapper.find(".dark-toggle").simulate("click")
+
+ // After the second click "dark" class is removed
+ expect(htmlElement.classList.contains("dark")).toBe(false)
+ })
+})