Skip to content

Commit 73569c3

Browse files
JacobCoffeeclaude
andauthored
feat: migrate OpenAPI documentation from Swagger to Scalar with custom theme (#137)
Co-authored-by: Claude <[email protected]>
1 parent 5fc5110 commit 73569c3

File tree

7 files changed

+175
-9
lines changed

7 files changed

+175
-9
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ When dispatching subagents:
292292

293293
- **Project Docs**: `docs/` (Sphinx RST format)
294294
- **Docker Guide**: `docs/docker-setup.md`
295-
- **API Docs**: http://localhost:8000/api/swagger (when running)
295+
- **API Docs (Scalar)**: http://localhost:8000/api/scalar (default, modern UI)
296+
- **API Docs (Swagger)**: http://localhost:8000/api/swagger (fallback)
296297
- **External**:
297298
- discord.py: https://discordpy.readthedocs.io/
298299
- Litestar: https://docs.litestar.dev/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ make docker-up
5757

5858
# 4. Access the application
5959
# API: http://localhost:8000
60-
# API Docs: http://localhost:8000/api/swagger
60+
# API Docs (Scalar): http://localhost:8000/api/scalar
61+
# API Docs (Swagger): http://localhost:8000/api/swagger
6162
```
6263

6364
**📚 Full Docker Guide**: See [docs/docker-setup.md](docs/docker-setup.md) for comprehensive documentation including:

docs/web/api/lib/openapi.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,29 @@ openapi
44

55
OpenAPI config
66

7+
The Byte Bot API uses `Scalar <https://scalar.com/>`_ as the default OpenAPI documentation UI,
8+
with Swagger UI available as a fallback option.
9+
10+
Accessing the API Documentation
11+
--------------------------------
12+
13+
* **Scalar UI (Default)**: http://localhost:8000/api/scalar
14+
* **Swagger UI (Fallback)**: http://localhost:8000/api/swagger
15+
* **OpenAPI Schema**: http://localhost:8000/api/openapi.json
16+
17+
Custom Theme
18+
------------
19+
20+
The Scalar UI uses a custom theme with Byte brand colors:
21+
22+
* Primary: ``#42b1a8`` (Byte Teal)
23+
* Secondary: ``#7bcebc`` (Byte Blue)
24+
* Accent: ``#abe6d2`` (Byte Light Blue)
25+
26+
The theme supports both light and dark modes with automatic detection.
27+
28+
Configuration
29+
-------------
30+
731
.. automodule:: byte_api.lib.openapi
832
:members:
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/* Byte Brand Custom Theme for Scalar OpenAPI Documentation */
2+
3+
/* Light Mode Variables */
4+
:root {
5+
/* Primary accent color - Byte Teal */
6+
--scalar-color-accent: #42b1a8;
7+
8+
/* Background colors */
9+
--scalar-background-1: #ebebe9; /* byte-white base */
10+
--scalar-background-2: #ffffff; /* slightly lighter */
11+
--scalar-background-3: #d4d4d2; /* slightly darker */
12+
13+
/* Text colors */
14+
--scalar-color-1: #0c0c0c; /* byte-dark primary text */
15+
--scalar-color-2: #3c3c3c; /* medium text */
16+
--scalar-color-3: #6c6c6c; /* light text */
17+
18+
/* Border color - Byte Blue secondary */
19+
--scalar-border-color: #7bcebc;
20+
21+
/* Sidebar */
22+
--scalar-sidebar-background-1: #42b1a8; /* byte-teal */
23+
--scalar-sidebar-background-2: #7bcebc; /* byte-blue */
24+
--scalar-sidebar-color-1: #ebebe9; /* light text on dark sidebar */
25+
--scalar-sidebar-color-2: #ffffff; /* white text */
26+
--scalar-sidebar-color-active: #abe6d2; /* byte-light-blue for active items */
27+
--scalar-sidebar-border-color: #7bcebc;
28+
29+
/* Status colors from DaisyUI theme */
30+
--scalar-color-green: #059669; /* success */
31+
--scalar-color-yellow: #d4a35a; /* byte-orange warning */
32+
--scalar-color-red: #fc7054; /* byte-red error */
33+
--scalar-color-blue: #7bcebc; /* byte-blue info */
34+
35+
/* Additional accents */
36+
--scalar-button-1: #42b1a8;
37+
--scalar-button-1-hover: #389e96;
38+
--scalar-button-2: #7bcebc;
39+
--scalar-button-2-hover: #6ab8a8;
40+
}
41+
42+
/* Dark Mode Variables */
43+
@media (prefers-color-scheme: dark) {
44+
:root {
45+
/* Primary accent - keep teal */
46+
--scalar-color-accent: #42b1a8;
47+
48+
/* Background colors - inverted */
49+
--scalar-background-1: #0c0c0c; /* byte-dark */
50+
--scalar-background-2: #1a1a1a; /* slightly lighter */
51+
--scalar-background-3: #2a2a2a; /* lighter still */
52+
53+
/* Text colors - inverted */
54+
--scalar-color-1: #ebebe9; /* byte-white for text */
55+
--scalar-color-2: #d4d4d2; /* medium text */
56+
--scalar-color-3: #a4a4a2; /* light text */
57+
58+
/* Border color */
59+
--scalar-border-color: #42b1a8;
60+
61+
/* Sidebar - darker in dark mode */
62+
--scalar-sidebar-background-1: #0c0c0c;
63+
--scalar-sidebar-background-2: #1a1a1a;
64+
--scalar-sidebar-color-1: #ebebe9;
65+
--scalar-sidebar-color-2: #ffffff;
66+
--scalar-sidebar-color-active: #42b1a8;
67+
--scalar-sidebar-border-color: #42b1a8;
68+
69+
/* Status colors remain consistent */
70+
--scalar-color-green: #059669;
71+
--scalar-color-yellow: #d4a35a;
72+
--scalar-color-red: #fc7054;
73+
--scalar-color-blue: #7bcebc;
74+
75+
/* Buttons in dark mode */
76+
--scalar-button-1: #42b1a8;
77+
--scalar-button-1-hover: #52c1b8;
78+
--scalar-button-2: #7bcebc;
79+
--scalar-button-2-hover: #8bdccc;
80+
}
81+
}
82+
83+
/* Force dark mode when .dark class is present */
84+
.dark {
85+
--scalar-color-accent: #42b1a8;
86+
--scalar-background-1: #0c0c0c;
87+
--scalar-background-2: #1a1a1a;
88+
--scalar-background-3: #2a2a2a;
89+
--scalar-color-1: #ebebe9;
90+
--scalar-color-2: #d4d4d2;
91+
--scalar-color-3: #a4a4a2;
92+
--scalar-border-color: #42b1a8;
93+
--scalar-sidebar-background-1: #0c0c0c;
94+
--scalar-sidebar-background-2: #1a1a1a;
95+
--scalar-sidebar-color-1: #ebebe9;
96+
--scalar-sidebar-color-2: #ffffff;
97+
--scalar-sidebar-color-active: #42b1a8;
98+
--scalar-sidebar-border-color: #42b1a8;
99+
--scalar-color-green: #059669;
100+
--scalar-color-yellow: #d4a35a;
101+
--scalar-color-red: #fc7054;
102+
--scalar-color-blue: #7bcebc;
103+
--scalar-button-1: #42b1a8;
104+
--scalar-button-1-hover: #52c1b8;
105+
--scalar-button-2: #7bcebc;
106+
--scalar-button-2-hover: #8bdccc;
107+
}

services/api/src/byte_api/lib/openapi.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from __future__ import annotations
44

55
from litestar.openapi.config import OpenAPIConfig
6+
from litestar.openapi.plugins import SwaggerRenderPlugin
67
from litestar.openapi.spec import Contact
78

89
from byte_api.lib import settings
10+
from byte_api.lib.scalar_theme import create_scalar_plugin
911

1012
__all__ = ("config",)
1113

@@ -17,8 +19,11 @@
1719
version=settings.openapi.VERSION,
1820
contact=Contact(name=settings.openapi.CONTACT_NAME, email=settings.openapi.CONTACT_EMAIL),
1921
use_handler_docstrings=True,
20-
root_schema_site="swagger",
2122
path=settings.openapi.PATH,
23+
render_plugins=[
24+
create_scalar_plugin(),
25+
SwaggerRenderPlugin(path="/swagger"),
26+
],
2227
)
2328
"""OpenAPI config for the project.
2429
See :class:`OpenAPISettings <.settings.OpenAPISettings>` for configuration.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Custom Scalar OpenAPI theme with Byte branding."""
2+
3+
from __future__ import annotations
4+
5+
from litestar.openapi.plugins import ScalarRenderPlugin
6+
7+
__all__ = ("create_scalar_plugin",)
8+
9+
10+
def create_scalar_plugin() -> ScalarRenderPlugin:
11+
"""Create a Scalar plugin with custom Byte branding.
12+
13+
Returns:
14+
Configured ScalarRenderPlugin with custom CSS
15+
"""
16+
return ScalarRenderPlugin(
17+
path="/scalar",
18+
css_url="/static/css/scalar-theme.css",
19+
)

tests/unit/api/test_openapi.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"test_openapi_config_created",
2121
"test_openapi_contact_info",
2222
"test_openapi_includes_all_endpoints",
23+
"test_openapi_render_plugins_configured",
24+
"test_openapi_scalar_primary_plugin",
2325
"test_openapi_schema_generation",
2426
"test_openapi_security_schemes",
2527
"test_openapi_servers_configured",
@@ -58,9 +60,13 @@ def test_openapi_uses_handler_docstrings() -> None:
5860
assert openapi.config.use_handler_docstrings is True
5961

6062

61-
def test_openapi_root_schema_site() -> None:
62-
"""Test OpenAPI root schema site is set to Swagger."""
63-
assert openapi.config.root_schema_site == "swagger"
63+
def test_openapi_render_plugins_configured() -> None:
64+
"""Test OpenAPI render plugins are configured with Scalar and Swagger."""
65+
assert openapi.config.render_plugins is not None
66+
assert len(openapi.config.render_plugins) >= 2
67+
plugin_types = [type(plugin).__name__ for plugin in openapi.config.render_plugins]
68+
assert "ScalarRenderPlugin" in plugin_types
69+
assert "SwaggerRenderPlugin" in plugin_types
6470

6571

6672
@pytest.mark.asyncio
@@ -330,9 +336,12 @@ def test_openapi_use_handler_docstrings_true() -> None:
330336
assert openapi.config.use_handler_docstrings is True
331337

332338

333-
def test_openapi_root_schema_site_swagger() -> None:
334-
"""Test root schema site is Swagger."""
335-
assert openapi.config.root_schema_site == "swagger"
339+
def test_openapi_scalar_primary_plugin() -> None:
340+
"""Test Scalar is the primary render plugin."""
341+
assert openapi.config.render_plugins is not None
342+
assert len(openapi.config.render_plugins) > 0
343+
first_plugin_type = type(openapi.config.render_plugins[0]).__name__
344+
assert first_plugin_type == "ScalarRenderPlugin"
336345

337346

338347
def test_openapi_config_immutable() -> None:

0 commit comments

Comments
 (0)