diff --git a/CLAUDE.md b/CLAUDE.md index d004756e..df759003 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -292,7 +292,8 @@ When dispatching subagents: - **Project Docs**: `docs/` (Sphinx RST format) - **Docker Guide**: `docs/docker-setup.md` -- **API Docs**: http://localhost:8000/api/swagger (when running) +- **API Docs (Scalar)**: http://localhost:8000/api/scalar (default, modern UI) +- **API Docs (Swagger)**: http://localhost:8000/api/swagger (fallback) - **External**: - discord.py: https://discordpy.readthedocs.io/ - Litestar: https://docs.litestar.dev/ diff --git a/README.md b/README.md index 196065f9..230fbc32 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ make docker-up # 4. Access the application # API: http://localhost:8000 -# API Docs: http://localhost:8000/api/swagger +# API Docs (Scalar): http://localhost:8000/api/scalar +# API Docs (Swagger): http://localhost:8000/api/swagger ``` **📚 Full Docker Guide**: See [docs/docker-setup.md](docs/docker-setup.md) for comprehensive documentation including: diff --git a/docs/web/api/lib/openapi.rst b/docs/web/api/lib/openapi.rst index 2a7f5073..d2621e9a 100644 --- a/docs/web/api/lib/openapi.rst +++ b/docs/web/api/lib/openapi.rst @@ -4,5 +4,29 @@ openapi OpenAPI config +The Byte Bot API uses `Scalar `_ as the default OpenAPI documentation UI, +with Swagger UI available as a fallback option. + +Accessing the API Documentation +-------------------------------- + +* **Scalar UI (Default)**: http://localhost:8000/api/scalar +* **Swagger UI (Fallback)**: http://localhost:8000/api/swagger +* **OpenAPI Schema**: http://localhost:8000/api/openapi.json + +Custom Theme +------------ + +The Scalar UI uses a custom theme with Byte brand colors: + +* Primary: ``#42b1a8`` (Byte Teal) +* Secondary: ``#7bcebc`` (Byte Blue) +* Accent: ``#abe6d2`` (Byte Light Blue) + +The theme supports both light and dark modes with automatic detection. + +Configuration +------------- + .. automodule:: byte_api.lib.openapi :members: diff --git a/services/api/src/byte_api/domain/web/resources/css/scalar-theme.css b/services/api/src/byte_api/domain/web/resources/css/scalar-theme.css new file mode 100644 index 00000000..4a0fdbd8 --- /dev/null +++ b/services/api/src/byte_api/domain/web/resources/css/scalar-theme.css @@ -0,0 +1,107 @@ +/* Byte Brand Custom Theme for Scalar OpenAPI Documentation */ + +/* Light Mode Variables */ +:root { + /* Primary accent color - Byte Teal */ + --scalar-color-accent: #42b1a8; + + /* Background colors */ + --scalar-background-1: #ebebe9; /* byte-white base */ + --scalar-background-2: #ffffff; /* slightly lighter */ + --scalar-background-3: #d4d4d2; /* slightly darker */ + + /* Text colors */ + --scalar-color-1: #0c0c0c; /* byte-dark primary text */ + --scalar-color-2: #3c3c3c; /* medium text */ + --scalar-color-3: #6c6c6c; /* light text */ + + /* Border color - Byte Blue secondary */ + --scalar-border-color: #7bcebc; + + /* Sidebar */ + --scalar-sidebar-background-1: #42b1a8; /* byte-teal */ + --scalar-sidebar-background-2: #7bcebc; /* byte-blue */ + --scalar-sidebar-color-1: #ebebe9; /* light text on dark sidebar */ + --scalar-sidebar-color-2: #ffffff; /* white text */ + --scalar-sidebar-color-active: #abe6d2; /* byte-light-blue for active items */ + --scalar-sidebar-border-color: #7bcebc; + + /* Status colors from DaisyUI theme */ + --scalar-color-green: #059669; /* success */ + --scalar-color-yellow: #d4a35a; /* byte-orange warning */ + --scalar-color-red: #fc7054; /* byte-red error */ + --scalar-color-blue: #7bcebc; /* byte-blue info */ + + /* Additional accents */ + --scalar-button-1: #42b1a8; + --scalar-button-1-hover: #389e96; + --scalar-button-2: #7bcebc; + --scalar-button-2-hover: #6ab8a8; +} + +/* Dark Mode Variables */ +@media (prefers-color-scheme: dark) { + :root { + /* Primary accent - keep teal */ + --scalar-color-accent: #42b1a8; + + /* Background colors - inverted */ + --scalar-background-1: #0c0c0c; /* byte-dark */ + --scalar-background-2: #1a1a1a; /* slightly lighter */ + --scalar-background-3: #2a2a2a; /* lighter still */ + + /* Text colors - inverted */ + --scalar-color-1: #ebebe9; /* byte-white for text */ + --scalar-color-2: #d4d4d2; /* medium text */ + --scalar-color-3: #a4a4a2; /* light text */ + + /* Border color */ + --scalar-border-color: #42b1a8; + + /* Sidebar - darker in dark mode */ + --scalar-sidebar-background-1: #0c0c0c; + --scalar-sidebar-background-2: #1a1a1a; + --scalar-sidebar-color-1: #ebebe9; + --scalar-sidebar-color-2: #ffffff; + --scalar-sidebar-color-active: #42b1a8; + --scalar-sidebar-border-color: #42b1a8; + + /* Status colors remain consistent */ + --scalar-color-green: #059669; + --scalar-color-yellow: #d4a35a; + --scalar-color-red: #fc7054; + --scalar-color-blue: #7bcebc; + + /* Buttons in dark mode */ + --scalar-button-1: #42b1a8; + --scalar-button-1-hover: #52c1b8; + --scalar-button-2: #7bcebc; + --scalar-button-2-hover: #8bdccc; + } +} + +/* Force dark mode when .dark class is present */ +.dark { + --scalar-color-accent: #42b1a8; + --scalar-background-1: #0c0c0c; + --scalar-background-2: #1a1a1a; + --scalar-background-3: #2a2a2a; + --scalar-color-1: #ebebe9; + --scalar-color-2: #d4d4d2; + --scalar-color-3: #a4a4a2; + --scalar-border-color: #42b1a8; + --scalar-sidebar-background-1: #0c0c0c; + --scalar-sidebar-background-2: #1a1a1a; + --scalar-sidebar-color-1: #ebebe9; + --scalar-sidebar-color-2: #ffffff; + --scalar-sidebar-color-active: #42b1a8; + --scalar-sidebar-border-color: #42b1a8; + --scalar-color-green: #059669; + --scalar-color-yellow: #d4a35a; + --scalar-color-red: #fc7054; + --scalar-color-blue: #7bcebc; + --scalar-button-1: #42b1a8; + --scalar-button-1-hover: #52c1b8; + --scalar-button-2: #7bcebc; + --scalar-button-2-hover: #8bdccc; +} diff --git a/services/api/src/byte_api/lib/openapi.py b/services/api/src/byte_api/lib/openapi.py index 0322eb65..43cbacd7 100644 --- a/services/api/src/byte_api/lib/openapi.py +++ b/services/api/src/byte_api/lib/openapi.py @@ -3,9 +3,11 @@ from __future__ import annotations from litestar.openapi.config import OpenAPIConfig +from litestar.openapi.plugins import SwaggerRenderPlugin from litestar.openapi.spec import Contact from byte_api.lib import settings +from byte_api.lib.scalar_theme import create_scalar_plugin __all__ = ("config",) @@ -17,8 +19,11 @@ version=settings.openapi.VERSION, contact=Contact(name=settings.openapi.CONTACT_NAME, email=settings.openapi.CONTACT_EMAIL), use_handler_docstrings=True, - root_schema_site="swagger", path=settings.openapi.PATH, + render_plugins=[ + create_scalar_plugin(), + SwaggerRenderPlugin(path="/swagger"), + ], ) """OpenAPI config for the project. See :class:`OpenAPISettings <.settings.OpenAPISettings>` for configuration. diff --git a/services/api/src/byte_api/lib/scalar_theme.py b/services/api/src/byte_api/lib/scalar_theme.py new file mode 100644 index 00000000..382345de --- /dev/null +++ b/services/api/src/byte_api/lib/scalar_theme.py @@ -0,0 +1,19 @@ +"""Custom Scalar OpenAPI theme with Byte branding.""" + +from __future__ import annotations + +from litestar.openapi.plugins import ScalarRenderPlugin + +__all__ = ("create_scalar_plugin",) + + +def create_scalar_plugin() -> ScalarRenderPlugin: + """Create a Scalar plugin with custom Byte branding. + + Returns: + Configured ScalarRenderPlugin with custom CSS + """ + return ScalarRenderPlugin( + path="/scalar", + css_url="/static/css/scalar-theme.css", + ) diff --git a/tests/unit/api/test_openapi.py b/tests/unit/api/test_openapi.py index 755d6945..243d2147 100644 --- a/tests/unit/api/test_openapi.py +++ b/tests/unit/api/test_openapi.py @@ -20,6 +20,8 @@ "test_openapi_config_created", "test_openapi_contact_info", "test_openapi_includes_all_endpoints", + "test_openapi_render_plugins_configured", + "test_openapi_scalar_primary_plugin", "test_openapi_schema_generation", "test_openapi_security_schemes", "test_openapi_servers_configured", @@ -58,9 +60,13 @@ def test_openapi_uses_handler_docstrings() -> None: assert openapi.config.use_handler_docstrings is True -def test_openapi_root_schema_site() -> None: - """Test OpenAPI root schema site is set to Swagger.""" - assert openapi.config.root_schema_site == "swagger" +def test_openapi_render_plugins_configured() -> None: + """Test OpenAPI render plugins are configured with Scalar and Swagger.""" + assert openapi.config.render_plugins is not None + assert len(openapi.config.render_plugins) >= 2 + plugin_types = [type(plugin).__name__ for plugin in openapi.config.render_plugins] + assert "ScalarRenderPlugin" in plugin_types + assert "SwaggerRenderPlugin" in plugin_types @pytest.mark.asyncio @@ -330,9 +336,12 @@ def test_openapi_use_handler_docstrings_true() -> None: assert openapi.config.use_handler_docstrings is True -def test_openapi_root_schema_site_swagger() -> None: - """Test root schema site is Swagger.""" - assert openapi.config.root_schema_site == "swagger" +def test_openapi_scalar_primary_plugin() -> None: + """Test Scalar is the primary render plugin.""" + assert openapi.config.render_plugins is not None + assert len(openapi.config.render_plugins) > 0 + first_plugin_type = type(openapi.config.render_plugins[0]).__name__ + assert first_plugin_type == "ScalarRenderPlugin" def test_openapi_config_immutable() -> None: