From 96e859bd27c2d3dcba531af0831495a52b3b0160 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Fri, 23 Jan 2026 16:38:10 -0500 Subject: [PATCH] fix: use native Swagger UI dark mode support (#440) --- app/globals.css | 275 +---------------------------------------- components/swagger.tsx | 27 ++++ package.json | 2 +- pnpm-lock.yaml | 33 +++-- 4 files changed, 49 insertions(+), 288 deletions(-) diff --git a/app/globals.css b/app/globals.css index da1927b1..bcc71ec8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -94,278 +94,13 @@ body { display: none; } -/* Base Swagger UI styling */ -.swagger-ui { - background-color: transparent; - font-family: inherit; -} - -/* ============================================ - Dark mode styling for Swagger UI - Fixes issue #440: dark mode text was unreadable - Light mode uses Swagger UI defaults (works fine) - ============================================ */ - -/* Dark mode text colors - excluding links (handled separately) */ -html.dark .swagger-ui .opblock-summary-path, -html.dark .swagger-ui .opblock-summary-path span, -html.dark .swagger-ui .opblock-tag, -html.dark .swagger-ui .opblock-tag small, -html.dark .swagger-ui .info .title, -html.dark .swagger-ui .info .title small, -html.dark .swagger-ui .info .title small pre, -html.dark .swagger-ui .info li, -html.dark .swagger-ui .info p, -html.dark .swagger-ui .info table, -html.dark .swagger-ui .info h1, -html.dark .swagger-ui .info h2, -html.dark .swagger-ui .info h3, -html.dark .swagger-ui .info h4, -html.dark .swagger-ui .info h5, -html.dark .swagger-ui .tab li, -html.dark .swagger-ui .model-title, -html.dark .swagger-ui .model .property.primitive, -html.dark .swagger-ui .responses-inner h4, -html.dark .swagger-ui .responses-inner h5, -html.dark .swagger-ui .response-col_status, -html.dark .swagger-ui .response-col_links, -html.dark .swagger-ui .opblock .opblock-summary-description, -html.dark .swagger-ui .opblock-description-wrapper p, -html.dark .swagger-ui .opblock-external-docs-wrapper, -html.dark .swagger-ui .opblock-title_normal, -html.dark .swagger-ui .parameter__name, -html.dark .swagger-ui .parameter__type, -html.dark .swagger-ui .parameter__deprecated, -html.dark .swagger-ui .parameter__in, -html.dark .swagger-ui .response-col_description__inner p, -html.dark .swagger-ui .response-col_description__inner span, -html.dark .swagger-ui .prop-type, -html.dark .swagger-ui .prop-format, -html.dark .swagger-ui table thead tr th, -html.dark .swagger-ui table thead tr td, -html.dark .swagger-ui table tbody tr th, -html.dark .swagger-ui table tbody tr td, -html.dark .swagger-ui .renderedMarkdown p, -html.dark .swagger-ui .renderedMarkdown li, -html.dark .swagger-ui .renderedMarkdown code, -html.dark .swagger-ui .markdown p, -html.dark .swagger-ui .markdown li, -html.dark .swagger-ui .markdown code { - color: var(--color-primary-200); -} - -/* Dark mode links - green for visibility */ -html.dark .swagger-ui a { - color: #6ee7b7; -} - -/* Dark mode code blocks */ -html.dark .swagger-ui .highlight-code, -html.dark .swagger-ui .microlight { - background-color: var(--color-primary-800) !important; -} - -html.dark .swagger-ui pre.microlight { - background-color: var(--color-primary-800) !important; - color: var(--color-primary-200) !important; -} - -html.dark .swagger-ui pre.microlight code { - color: var(--color-primary-200) !important; -} - -/* Dark mode operation blocks - GET */ -html.dark .swagger-ui .opblock.opblock-get { - background: rgba(97, 175, 254, 0.15); - border-color: #61affe; -} - -html.dark .swagger-ui .opblock.opblock-get .opblock-summary { - border-color: #61affe; -} - -/* Dark mode operation blocks - POST */ -html.dark .swagger-ui .opblock.opblock-post { - background: rgba(73, 204, 144, 0.15); - border-color: #49cc90; -} - -html.dark .swagger-ui .opblock.opblock-post .opblock-summary { - border-color: #49cc90; -} - -/* Dark mode operation blocks - PUT */ -html.dark .swagger-ui .opblock.opblock-put { - background: rgba(252, 161, 48, 0.15); - border-color: #fca130; -} - -html.dark .swagger-ui .opblock.opblock-put .opblock-summary { - border-color: #fca130; -} - -/* Dark mode operation blocks - DELETE */ -html.dark .swagger-ui .opblock.opblock-delete { - background: rgba(249, 62, 62, 0.15); - border-color: #f93e3e; -} - -html.dark .swagger-ui .opblock.opblock-delete .opblock-summary { - border-color: #f93e3e; -} - -/* Dark mode operation blocks - PATCH */ -html.dark .swagger-ui .opblock.opblock-patch { - background: rgba(80, 227, 194, 0.15); - border-color: #50e3c2; -} - -html.dark .swagger-ui .opblock.opblock-patch .opblock-summary { - border-color: #50e3c2; -} - -/* Dark mode expanded operation block body */ -html.dark .swagger-ui .opblock-body pre.microlight { - background-color: #1a1a1a !important; - border: 1px solid var(--color-primary-700); -} - -/* Dark mode response section */ -html.dark .swagger-ui .responses-wrapper, -html.dark .swagger-ui .response { - background-color: transparent; -} - -html.dark .swagger-ui .response-col_description__inner { - background-color: transparent; -} - -/* Dark mode models section */ -html.dark .swagger-ui .models { - border-color: var(--color-primary-700); -} - -html.dark .swagger-ui .model-container { - background-color: #1a1a1a; - border-color: var(--color-primary-700); -} - -html.dark .swagger-ui .model-box { - background-color: var(--color-primary-800) !important; +/* Prevent Swagger UI's dark-mode from overriding the page background */ +html.dark-mode { + background: rgb(var(--nextra-bg)); } -html.dark .swagger-ui .model { - color: var(--color-primary-200); -} - -/* Dark mode tables */ -html.dark .swagger-ui table { - background-color: transparent; -} - -html.dark .swagger-ui table thead tr th, -html.dark .swagger-ui table thead tr td { - border-bottom-color: var(--color-primary-700); -} - -html.dark .swagger-ui table tbody tr td { - border-bottom-color: #333; -} - -/* Dark mode buttons and interactive elements */ -html.dark .swagger-ui .btn { - background-color: var(--color-primary-700); - color: var(--color-primary-200); - border-color: var(--color-primary-600); -} - -html.dark .swagger-ui .btn:hover { - background-color: var(--color-primary-600); -} - -/* Dark mode tab styling */ -html.dark .swagger-ui .tab li { - background-color: transparent; - border-color: var(--color-primary-700); -} - -html.dark .swagger-ui .tab li.active { - background-color: var(--color-primary-800); -} - -/* Dark mode parameters table */ -html.dark .swagger-ui .parameters-col_description { - color: var(--color-primary-400); -} - -html.dark .swagger-ui .parameter__name.required::after { - color: #f87171; -} - -/* Dark mode copy button - target the SVG inside */ -html.dark .swagger-ui .copy-to-clipboard { - background-color: var(--color-primary-700); -} - -html.dark .swagger-ui .copy-to-clipboard svg { - fill: var(--color-primary-200); -} - -/* Dark mode expand/collapse icons */ -html.dark .swagger-ui .expand-operation svg, -html.dark .swagger-ui .expand-methods svg { - fill: var(--color-primary-400); -} - -/* Dark mode arrow icons */ -html.dark .swagger-ui .arrow { - fill: var(--color-primary-400); -} - -/* Dark mode loading spinner */ -html.dark .swagger-ui .loading-container .loading::after { - border-color: var(--color-primary-700); - border-top-color: var(--color-primary-200); -} - -/* Dark mode section borders */ -html.dark .swagger-ui section.models.is-open { - border-color: var(--color-primary-700); -} - -html.dark .swagger-ui .opblock-tag-section { - border-bottom-color: var(--color-primary-700); -} - -/* Dark mode section headers (Parameters, Request body, Responses) */ -html.dark .swagger-ui .opblock-section-header { - background-color: var(--color-primary-800); - border-bottom-color: var(--color-primary-700); -} - -html.dark .swagger-ui .opblock-section-header h4, -html.dark .swagger-ui .opblock-section-header label { - color: var(--color-primary-200); -} - -/* Dark mode Example Value / Model tabs */ -html.dark .swagger-ui .tab li button, -html.dark .swagger-ui .tab li button.tablinks { - color: var(--color-primary-300); +/* Base Swagger UI styling - keep transparent background for integration */ +.swagger-ui { background-color: transparent; } -html.dark .swagger-ui .tab li button.tablinks.active, -html.dark .swagger-ui .tab li button:focus { - color: var(--color-primary-100); -} - -/* Dark mode model/schema display */ -html.dark .swagger-ui .model-toggle::after { - background-color: var(--color-primary-400); -} - -html.dark .swagger-ui .model span, -html.dark .swagger-ui .model-title__text { - color: var(--color-primary-200); -} diff --git a/components/swagger.tsx b/components/swagger.tsx index 5b3cde0d..8d0b7590 100644 --- a/components/swagger.tsx +++ b/components/swagger.tsx @@ -1,6 +1,7 @@ "use client"; import dynamic from "next/dynamic"; +import { useEffect } from "react"; import type { SwaggerUIProps } from "swagger-ui-react"; const SwaggerUI = dynamic( @@ -13,7 +14,33 @@ const SwaggerUI = dynamic( import "swagger-ui-react/swagger-ui.css"; +// Sync Nextra's "dark" class with Swagger UI's "dark-mode" class +function useSyncDarkMode() { + useEffect(() => { + const html = document.documentElement; + + const syncDarkMode = () => { + html.classList.toggle("dark-mode", html.classList.contains("dark")); + }; + + syncDarkMode(); + + const observer = new MutationObserver(() => { + syncDarkMode(); + }); + + observer.observe(html, { attributes: true, attributeFilter: ["class"] }); + + return () => { + observer.disconnect(); + html.classList.remove("dark-mode"); + }; + }, []); +} + export function Swagger() { + useSyncDarkMode(); + return ( =16.8.0 <20' react-dom: '>=16.8.0 <20' @@ -4195,11 +4199,6 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -7560,6 +7559,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@0.5.0: {} jsesc@2.5.2: {} @@ -8665,7 +8668,7 @@ snapshots: dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.2.3 - use-sync-external-store: 1.5.0(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: '@types/react': 19.1.6 redux: 5.0.1 @@ -9145,7 +9148,7 @@ snapshots: '@swaggerexpert/cookie': 2.0.2 deepmerge: 4.3.1 fast-json-patch: 3.1.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 neotraverse: 0.6.18 node-abort-controller: 3.1.1 node-fetch-commonjs: 3.3.2 @@ -9156,7 +9159,7 @@ snapshots: transitivePeerDependencies: - debug - swagger-ui-react@5.30.2(@types/react@19.1.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + swagger-ui-react@5.31.0(@types/react@19.1.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime-corejs3': 7.27.4 '@scarf/scarf': 1.4.0 @@ -9169,7 +9172,7 @@ snapshots: ieee754: 1.2.1 immutable: 3.8.2 js-file-download: 0.4.12 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 prop-types: 15.8.1 randexp: 0.5.3 @@ -9416,10 +9419,6 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-sync-external-store@1.5.0(react@19.2.3): - dependencies: - react: 19.2.3 - use-sync-external-store@1.6.0(react@19.2.3): dependencies: react: 19.2.3