diff --git a/bazel/toolchains/BUILD b/bazel/toolchains/BUILD index c2156b3ff..fdc19b88a 100644 --- a/bazel/toolchains/BUILD +++ b/bazel/toolchains/BUILD @@ -11,6 +11,10 @@ py_binary( srcs = ["@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py"], data = [ "@score_tooling//tools/sphinx:plantuml", + # Expose the project-level custom CSS to all sub-doc Sphinx builds + # (e.g. dependable_element docs) which run in separate Bazel sandboxes + # and cannot access docs/sphinx/_static directly. + "//docs/sphinx:custom_css", ], exec_compatible_with = ["@platforms//os:linux"], main = "@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py", diff --git a/bazel/toolchains/template/conf.template.py b/bazel/toolchains/template/conf.template.py index c406140cc..99a881f02 100644 --- a/bazel/toolchains/template/conf.template.py +++ b/bazel/toolchains/template/conf.template.py @@ -99,8 +99,14 @@ 'show_prev_next': True, # Logo configuration + # Sub-doc builds (e.g. dependable_element) are embedded one level deep + # inside the main docs output. Add 'link': '../index.html' so the logo + # navigates back to the main docs. The main docs build has a 'docs/' + # directory sibling in the Bazel sandbox; sub-doc builds do not — use that + # to distinguish the two cases. 'logo': { 'text': 'Eclipse S-CORE', + **({} if (Path(__file__).parent / "docs").is_dir() else {'link': '../index.html'}), }, # External links - S-CORE GitHub @@ -111,12 +117,32 @@ 'icon': 'fab fa-github', } ], + } # Enable numref for cross-references numfig = True +# Static assets and custom CSS +# html_static_path is relative to confdir (where this conf.py lives). +# In a local build confdir == the docs source dir, so '_static' is a direct +# sibling. In the Bazel sandbox the generated conf.py sits one level above the +# actual source tree (sphinx_doc/conf.py vs sphinx_doc/docs/sphinx/_static), +# so we search for the _static directory instead of hard-coding the path. +_conf_dir = Path(__file__).parent +_static_local = _conf_dir / "_static" +if _static_local.exists(): + html_static_path = ["_static"] +else: + _static_found = next( + (p for p in _conf_dir.rglob("_static") if p.is_dir()), None + ) + html_static_path = [str(_static_found)] if _static_found else [] + +# html_css_files is populated after the runfiles section below once we know +# whether default_custom.css is reachable (local _static or runfiles). + # Load external needs and log configuration needs_external_needs = bazel_sphinx_needs.load_external_needs() bazel_sphinx_needs.log_config_info(project) @@ -149,6 +175,51 @@ plantuml = f"{_plantuml_path} -Playout=smetana" plantuml_output_format = "svg_obj" +# Resolve default_custom.css so that sub-doc builds (e.g. dependable_element +# docs) which run in a separate Bazel sandbox also get the project CSS theme. +# The file is declared as data on the sphinx_build binary so it is always +# present in the runfiles tree. +_custom_css_src = None +_css_rloc = r.Rlocation( + "_main/docs/sphinx/_static/css/default_custom.css", source_repo="" +) +if _css_rloc and Path(_css_rloc).exists(): + _custom_css_src = Path(_css_rloc) + logger.info(f"Custom CSS resolved from runfiles: {_custom_css_src}") +else: + # Fallback: already present in the locally discovered _static tree + for _sp in html_static_path: + _candidate_css = Path(_sp) / "css" / "default_custom.css" + if _candidate_css.exists(): + _custom_css_src = _candidate_css + break + +# Only generate the tag when we are certain the file will be available. +html_css_files = ["css/default_custom.css"] if _custom_css_src else [] + + +def _inject_custom_css(app, exception): + """Write default_custom.css directly into the Sphinx output _static/css/ + directory after the build finishes. + + Using build-finished (rather than builder-inited + tempfile) avoids any + sandbox write-permission issues in CI: app.outdir is the declared Bazel + output tree, which is always writable during an action. Sphinx has already + run copy_static_files() before this event fires, so our file is not + overwritten, but the tag generated via html_css_files is already + present in every page.""" + if exception is not None or _custom_css_src is None: + return + import shutil + + css_dir = Path(app.outdir) / "_static" / "css" + css_dir.mkdir(parents=True, exist_ok=True) + dest = css_dir / "default_custom.css" + if not dest.exists(): + shutil.copy(str(_custom_css_src), str(dest)) + logger.info(f"Custom CSS written to output: {dest}") + def setup(app): + app.connect("build-finished", _inject_custom_css) return bazel_sphinx_needs.setup_sphinx_extension(app, needs_external_needs) diff --git a/docs/sphinx/BUILD b/docs/sphinx/BUILD index 79d1ed206..89ff668e5 100644 --- a/docs/sphinx/BUILD +++ b/docs/sphinx/BUILD @@ -25,15 +25,28 @@ sphinx_docs_library( srcs = ["//score/mw/com/design/doxygen_build:generate_doxygen"], ) +# Expose version flyout assets so docs/sphinx/utils targets can reference them +exports_files( + [ + "_static/css/version_flyout.css", + "_static/js/version_flyout.js", + ], + visibility = ["//docs/sphinx/utils:__pkg__"], +) + # Static assets for Sphinx documentation filegroup( name = "static_assets", srcs = glob(["_static/**/*"]), ) -exports_files( - glob(["_static/**/*"]), - visibility = ["//docs/sphinx/utils:__pkg__"], +# Custom CSS exposed so it can be added to the sphinx_build runfiles and +# therefore be available to sub-doc builds (e.g. dependable_element docs) +# that are built in a separate Bazel sandbox. +filegroup( + name = "custom_css", + srcs = ["_static/css/default_custom.css"], + visibility = ["//bazel/toolchains:__pkg__"], ) # Generate RST files from @api tagged items using custom Starlark rule diff --git a/docs/sphinx/_static/css/default_custom.css b/docs/sphinx/_static/css/default_custom.css index 64fcbacd3..b55c6bfeb 100644 --- a/docs/sphinx/_static/css/default_custom.css +++ b/docs/sphinx/_static/css/default_custom.css @@ -79,16 +79,16 @@ html[data-theme="dark"] { /* ============================================================================ * Navigation Styles * ============================================================================ */ -/* Search box in navbar/header - always light background with dark text */ +/* Search box in navbar/header */ .bd-header input.search-input, .bd-header .search-button-field, .bd-header input[type="search"], .navbar input.search-input, .navbar .search-button-field, .navbar input[type="search"] { - color: #333333 ; - background-color: #ffffff ; - border: none ; + color: #FFFFFF !important; + background-color: transparent !important; + border: none !important; } .bd-header input.search-input::placeholder, @@ -141,16 +141,6 @@ html .pst-navbar-icon:hover { border-bottom: 1px solid var(--pst-color-primary); } -/* Version Switcher Button */ -button.btn.version-switcher__button { - color: #FFF; -} - -/* Version Switcher Menu */ -.version-switcher__menu a.list-group-item { - color: #FFF; -} - /* Primary navigation styles */ .navbar-brand { color: #FFFFFF ; @@ -186,97 +176,45 @@ button.btn.version-switcher__button { color: #FFFFFF ; } -/* Override for search input - must come after the wildcard rule above */ -/* Light theme search box - light background with dark/purple text */ -html[data-theme="light"] .bd-header input, -html[data-theme="light"] .bd-header input.search-input, -html[data-theme="light"] .bd-header input[type="search"], -html[data-theme="light"] .bd-header .search-button-field input, -html[data-theme="light"] .bd-header .search-button__button, -html[data-theme="light"] .bd-header button.search-button__button, -html[data-theme="light"] .bd-header .search-button, -html[data-theme="light"] .navbar input, -html[data-theme="light"] .navbar input.search-input, -html[data-theme="light"] .navbar input[type="search"] { - color: #7c4daa; - background-color: #ffffff; - transition: background-color 0.2s ease, color 0.2s ease; -} - -/* Hover effect for light theme search box */ -html[data-theme="light"] .bd-header .search-button__button:hover, -html[data-theme="light"] .bd-header button.search-button__button:hover { - background-color: #e8d4f5 !important; - color: #7c4daa !important; -} - -/* Light theme - search button text and icon - force same color as text */ -html[data-theme="light"] .bd-header .search-button__default-text, -html[data-theme="light"] .bd-header .search-button__kbd-shortcut, -html[data-theme="light"] .navbar .search-button__default-text, -html[data-theme="light"] .navbar .search-button__kbd-shortcut { - color: #7c4daa; -} - -/* Light theme - SVG icon - must match text color */ -html[data-theme="light"] .bd-header .search-button svg, -html[data-theme="light"] .bd-header .search-button svg *, -html[data-theme="light"] .bd-header .search-button svg path, -html[data-theme="light"] .bd-header .search-button svg circle, -html[data-theme="light"] .navbar .search-button svg, -html[data-theme="light"] .navbar .search-button svg *, -html[data-theme="light"] .navbar .search-button svg path, -html[data-theme="light"] .navbar .search-button svg circle { - color: #7c4daa; - fill: none; - stroke: #7c4daa; - stroke-width: 2; -} - -/* Dark theme search box - gray background with white text */ -html[data-theme="dark"] .bd-header input, -html[data-theme="dark"] .bd-header input.search-input, -html[data-theme="dark"] .bd-header input[type="search"], -html[data-theme="dark"] .bd-header .search-button-field input, -html[data-theme="dark"] .bd-header .search-button__button, -html[data-theme="dark"] .bd-header button.search-button__button, -html[data-theme="dark"] .bd-header .search-button, -html[data-theme="dark"] .navbar input, -html[data-theme="dark"] .navbar input.search-input, -html[data-theme="dark"] .navbar input[type="search"] { - color: #ffffff; - background-color: #4a4a4a; - transition: background-color 0.2s ease, color 0.2s ease; -} - -/* Hover effect for dark theme search box */ -html[data-theme="dark"] .bd-header .search-button__button:hover, -html[data-theme="dark"] .bd-header button.search-button__button:hover { - background-color: #d4c0e8 !important; - color: #2D1942 !important; -} - -/* Dark theme - search button text and icon */ -html[data-theme="dark"] .bd-header .search-button__default-text, -html[data-theme="dark"] .bd-header .search-button__kbd-shortcut, -html[data-theme="dark"] .navbar .search-button__default-text, -html[data-theme="dark"] .navbar .search-button__kbd-shortcut { - color: #ffffff; -} - -/* Dark theme - SVG icon - must match text color */ -html[data-theme="dark"] .bd-header .search-button svg, -html[data-theme="dark"] .bd-header .search-button svg *, -html[data-theme="dark"] .bd-header .search-button svg path, -html[data-theme="dark"] .bd-header .search-button svg circle, -html[data-theme="dark"] .navbar .search-button svg, -html[data-theme="dark"] .navbar .search-button svg *, -html[data-theme="dark"] .navbar .search-button svg path, -html[data-theme="dark"] .navbar .search-button svg circle { - color: #ffffff; - fill: none; - stroke: #ffffff; - stroke-width: 2; +/* Header search button styling */ +.bd-header .search-button__button, +.bd-header button.search-button__button, +.bd-header .search-button { + color: #e6c8ff !important; + background-color: rgba(214, 171, 255, 0.18) !important; + border: 2px solid #b784e4 !important; + border-radius: 999px !important; + box-shadow: none !important; + transition: background-color 0.2s ease, border-color 0.2s ease, + color 0.2s ease !important; +} + +.bd-header .search-button__button:hover, +.bd-header button.search-button__button:hover { + background-color: rgba(214, 171, 255, 0.24) !important; + border-color: #c89af0 !important; + color: #f1dbff !important; +} + +.bd-header .search-button__default-text, +.bd-header .search-button__kbd-shortcut, +.navbar .search-button__default-text, +.navbar .search-button__kbd-shortcut { + color: #e6c8ff !important; +} + +.bd-header .search-button svg, +.bd-header .search-button svg *, +.bd-header .search-button svg path, +.bd-header .search-button svg circle, +.navbar .search-button svg, +.navbar .search-button svg *, +.navbar .search-button svg path, +.navbar .search-button svg circle { + color: #f3e5ff !important; + fill: none !important; + stroke: #f3e5ff !important; + stroke-width: 2 !important; } .bd-header a { diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index 04be6b9b0..82cf2baef 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -106,6 +106,7 @@ 'icon': 'fab fa-github', } ], + } # Add custom styling @@ -113,6 +114,8 @@ html_css_files = [ 'css/default_custom.css', ] +# Note: version_flyout.css and version_flyout.js are injected by the +# deploy workflow via _shared/ paths so they load once across all versions. # -- Breathe configuration -- # Doxygen XML output path (provided by sphinx_docs_library) diff --git a/docs/sphinx/utils/assemble_publish_tree.py b/docs/sphinx/utils/assemble_publish_tree.py index 3de59a261..687a5413b 100644 --- a/docs/sphinx/utils/assemble_publish_tree.py +++ b/docs/sphinx/utils/assemble_publish_tree.py @@ -39,6 +39,7 @@ import argparse import json +import os import pathlib import shutil import sys @@ -48,6 +49,18 @@ _CSS = _STATIC / "css" / "version_flyout.css" _JS = _STATIC / "js" / "version_flyout.js" +# When run via `bazel run`, the CWD is not necessarily the workspace root. +# Bazel sets BUILD_WORKSPACE_DIRECTORY to the workspace root so that scripts +# can resolve relative paths passed from the workflow correctly. +_WORKSPACE = pathlib.Path(os.environ.get("BUILD_WORKSPACE_DIRECTORY", os.getcwd())) + + +def _resolve(p: str) -> pathlib.Path: + """Resolve a path relative to the Bazel workspace root if not absolute.""" + path = pathlib.Path(p) + return path if path.is_absolute() else _WORKSPACE / path + + def main() -> int: parser = argparse.ArgumentParser( description="Assemble the GitHub Pages publish tree for versioned Sphinx docs." @@ -66,8 +79,8 @@ def main() -> int: help="Path to the root index.html (redirect page)") args = parser.parse_args() - publish = pathlib.Path(args.publish_dir) - docs_out = pathlib.Path(args.docs_output) + publish = _resolve(args.publish_dir) + docs_out = _resolve(args.docs_output) version = args.version is_tag = args.is_tag == "true" repo_url = args.repo_url.rstrip("/") @@ -107,7 +120,7 @@ def main() -> int: shutil.copy2(_JS, shared_js_dir / _JS.name) # ── Root files ──────────────────────────────────────────────────────────── - shutil.copy2(args.root_index, publish / "index.html") + shutil.copy2(_resolve(args.root_index), publish / "index.html") (publish / ".nojekyll").touch() # ── switcher.json ─────────────────────────────────────────────────────────