Let the API docs follow the system theme
The app has a dark theme. /api/docs was the one page that stayed white when the rest of the site went dark, which is exactly the page somebody has open beside the app while they work. Redoc does not read prefers-color-scheme, and the <redoc spec-url> element it was using takes no theme at all - so the page now initialises through Redoc.init, which does. Light keeps the default look; dark names the colours Redoc otherwise derives from the background and gets wrong: body text, the sidebar, and the right-hand sample panel, which came out dark-on-dark. It re-initialises on a live scheme change rather than restyling, because Redoc bakes its colours into styled-components at init - a CSS override reaches the chrome and misses the content. The listener has the addListener fallback for the older MediaQueryList, since a shop-floor kiosk browser is exactly where this gets read. Nothing new is loaded: same vendored bundle, same spec, no font, no CDN. A test pins that, because theming is the kind of change that quietly introduces one. The try-it page is left light. Swagger UI 5 ships no dark theme, so it would mean vendoring a community stylesheet that fights upstream on every bump - and it is a development tool that is off in production anyway.
This commit is contained in:
@@ -78,22 +78,71 @@ def docs_index():
|
||||
tryit_note = ''
|
||||
if _tryit_is_mounted():
|
||||
tryit_note = (
|
||||
'<p style="font:14px/1.4 system-ui,sans-serif;margin:0;'
|
||||
'padding:10px 16px;background:#eef2f7;border-bottom:1px solid #cbd5e1">'
|
||||
'<p class="docs-note">'
|
||||
'This page is read-only. To send a real request against this '
|
||||
'server, use <a href="%s">the try-it view</a>.</p>'
|
||||
% url_for(_TRYIT_ENDPOINT)
|
||||
)
|
||||
# DARK MODE. Redoc does not read prefers-color-scheme on its own, and
|
||||
# the <redoc spec-url> element takes no theme - so the page goes through
|
||||
# Redoc.init instead, which does. The app has a dark theme and this was
|
||||
# the one page that stayed white when the rest of the site went dark.
|
||||
#
|
||||
# The page RE-INITIALISES when the OS scheme flips rather than restyling:
|
||||
# Redoc bakes its colours into styled-components at init, so a CSS
|
||||
# override reaches the chrome and misses the content.
|
||||
page = (
|
||||
'<!doctype html><html><head>'
|
||||
'<title>ShopDB API</title>'
|
||||
'<meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
'<style>body{margin:0;padding:0}</style></head><body>'
|
||||
'<style>'
|
||||
':root{color-scheme:light dark}'
|
||||
'body{margin:0;padding:0;background:#fff}'
|
||||
'.docs-note{font:14px/1.4 system-ui,sans-serif;margin:0;'
|
||||
'padding:10px 16px;background:#eef2f7;color:#0f172a;'
|
||||
'border-bottom:1px solid #cbd5e1}'
|
||||
'.docs-note a{color:#1d4ed8}'
|
||||
'@media (prefers-color-scheme: dark){'
|
||||
'body{background:#0f172a}'
|
||||
'.docs-note{background:#1e293b;color:#e2e8f0;'
|
||||
'border-bottom-color:#334155}'
|
||||
'.docs-note a{color:#7dd3fc}'
|
||||
'}'
|
||||
'</style></head><body>'
|
||||
'%s'
|
||||
'<redoc spec-url="%s"></redoc>'
|
||||
'<div id="redoc"></div>'
|
||||
'<script src="%s"></script>'
|
||||
'</body></html>' % (tryit_note, spec_url, redoc_url)
|
||||
'<script>'
|
||||
'var SPEC = %s;'
|
||||
# Redoc computes text and the right-hand sample panel from the
|
||||
# background and gets both wrong on a dark one, so they are named
|
||||
# rather than left to it.
|
||||
'var DARK={colors:{primary:{main:"#7dd3fc"},'
|
||||
'text:{primary:"#e2e8f0",secondary:"#94a3b8"},'
|
||||
'border:{dark:"#334155",light:"#1e293b"}},'
|
||||
'sidebar:{backgroundColor:"#111827",textColor:"#e2e8f0",'
|
||||
'activeTextColor:"#7dd3fc"},'
|
||||
'rightPanel:{backgroundColor:"#020617",textColor:"#e2e8f0"},'
|
||||
'codeBlock:{backgroundColor:"#0b1220"},'
|
||||
'schema:{nestedBackground:"#0b1220"},'
|
||||
'typography:{fontFamily:"system-ui,sans-serif",'
|
||||
'code:{color:"#e2e8f0",backgroundColor:"#0b1220"}}};'
|
||||
'var LIGHT={colors:{primary:{main:"#1d4ed8"}},'
|
||||
'typography:{fontFamily:"system-ui,sans-serif"}};'
|
||||
'var q=window.matchMedia("(prefers-color-scheme: dark)");'
|
||||
'function draw(){'
|
||||
'document.getElementById("redoc").innerHTML="";'
|
||||
'Redoc.init(SPEC,{theme:q.matches?DARK:LIGHT},'
|
||||
'document.getElementById("redoc"));'
|
||||
'}'
|
||||
'draw();'
|
||||
# addEventListener is missing from MediaQueryList in older WebViews,
|
||||
# and a shop-floor kiosk browser is exactly where this gets read.
|
||||
'if(q.addEventListener){q.addEventListener("change",draw);}'
|
||||
'else if(q.addListener){q.addListener(draw);}'
|
||||
'</script>'
|
||||
'</body></html>' % (tryit_note, redoc_url, json.dumps(spec_url))
|
||||
)
|
||||
return Response(page, mimetype='text/html')
|
||||
|
||||
|
||||
@@ -247,3 +247,37 @@ def test_the_spec_declares_the_mount_it_is_served_from(enabled_client):
|
||||
# and duplicating it into every path would double it under a subpath.
|
||||
assert '/api/apitokens' in mounted['paths']
|
||||
assert '/shopdb/api/apitokens' not in mounted['paths']
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The read-only page follows the OS theme
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_redoc_page_follows_the_system_theme(client):
|
||||
"""The app has a dark theme; /api/docs was the one page that stayed white.
|
||||
|
||||
Redoc ignores prefers-color-scheme and the <redoc spec-url> element takes no
|
||||
theme, so the page has to go through Redoc.init - which is also why this
|
||||
test pins the mechanism and not just the CSS. A future edit that puts the
|
||||
element back would lose dark mode without changing a single colour.
|
||||
"""
|
||||
page = client.get('/api/docs/').get_data(as_text=True)
|
||||
|
||||
assert 'Redoc.init' in page
|
||||
assert '<redoc spec-url' not in page, 'the element takes no theme'
|
||||
assert '@media (prefers-color-scheme: dark)' in page
|
||||
assert 'var DARK=' in page and 'var LIGHT=' in page
|
||||
|
||||
# Re-render on a live scheme change, with the fallback for the older
|
||||
# MediaQueryList that a kiosk browser may still have.
|
||||
assert 'addEventListener("change",draw)' in page
|
||||
assert 'q.addListener(draw)' in page
|
||||
|
||||
|
||||
def test_the_docs_page_still_loads_nothing_from_another_origin(client):
|
||||
"""Theming is the kind of change that quietly introduces a font or a CDN."""
|
||||
import re as _re
|
||||
|
||||
page = client.get('/api/docs/').get_data(as_text=True)
|
||||
assert not _re.findall(r'(?:src|href)\s*=\s*[\'"](?:https?:)?//', page)
|
||||
|
||||
Reference in New Issue
Block a user