From 24608d6acace7d90a31b69412a302c406ce32ea5 Mon Sep 17 00:00:00 2001 From: Tobias Kaestner Date: Tue, 13 Oct 2026 21:55:35 +0200 Subject: [PATCH 1/2] fix: sphinx: a space after the field label of a need card field_line() wrote the prefix "Level:" without a space. In the PDF the label and the value follow each other in one line, so it showed "Level:software". The prefix is now "Level: ". In the HTML card the label is a grid column, and the browser drops the space at its end. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tobias Kaestner --- sphinx/_scripts/_tests/test_need_layouts.py | 4 ++-- sphinx/_scripts/need_layouts.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sphinx/_scripts/_tests/test_need_layouts.py b/sphinx/_scripts/_tests/test_need_layouts.py index d0620ea..83ca2a2 100644 --- a/sphinx/_scripts/_tests/test_need_layouts.py +++ b/sphinx/_scripts/_tests/test_need_layouts.py @@ -39,7 +39,7 @@ def test_the_card_has_the_head_the_fields_and_the_links(): '<>', ] assert layout["layout"]["meta"] == [ - '<>', + '<>', '<>', ] @@ -71,7 +71,7 @@ def test_a_hidden_backlink_is_not_shown(app): assert "verified by" in req and "SPEC_1" in req assert "covered by" not in req and "RES_1" not in req assert "needs_layout_zdocs_slim" in req - assert 'State:' in req + assert 'State: ' in req @pytest.mark.sphinx("html", srcdir=_ROOT) diff --git a/sphinx/_scripts/need_layouts.py b/sphinx/_scripts/need_layouts.py index 5d392b8..1f76f1a 100644 --- a/sphinx/_scripts/need_layouts.py +++ b/sphinx/_scripts/need_layouts.py @@ -66,11 +66,16 @@ def links_line(hide_backlinks=()): def field_line(name, label): - """The layout line for field ``name``, shown as ``label:``.""" + """The layout line for field ``name``, shown as ``label: value``. + + The space after the colon is for the PDF, where the label and the value + follow each other in one line. In the HTML card, the label is a grid + column and the browser drops the space at its end. + """ _check_names("fields", [name]) if not _LABEL.match(label): raise ValueError(f"fields: the label {label!r} of {name!r} must be plain words") - return f'<>' + return f'<>' def need_layout(fields=None, hide_backlinks=(), pill="status"): From 52af3e279eba6da9a18f5c0c7d2ccc934a56fc24 Mon Sep 17 00:00:00 2001 From: Tobias Kaestner Date: Tue, 13 Oct 2026 21:55:35 +0200 Subject: [PATCH 2/2] feat: sphinx: the need card colors as variables, with dark values zdocs-needs.css had 27 fixed colors: the white card, the field row, the lines, the ID, the pills, the adequacy chips and the preview popup. In a dark theme the cards were white boxes, and a theme had to override each rule. Every color of the card is now a --zd-* variable on :root. A theme sets the variables only. Dark values apply when has data-theme="dark" or the class dark-mode, the usual switches of a dark mode for sphinx_rtd_theme. There is no prefers-color-scheme rule, because sphinx_rtd_theme has no dark mode of its own. The adequacy chips now use the colors of the pills. The type label was the type color at lightness 0.52 with four times its chroma, on a head with 55% of the type color. A saturated type left the sRGB gamut: #C2185B gave a contrast of 1.46:1. The chroma is now capped (--zd-accent-c, 0.14), the label is darker (0.34) and the head has 35% of the type color. The label and the title then have at least 4.5:1 on the head for any type color, and at least 6.7:1 for the palettes of Zephyr safety and safety-toolbox. The dark values (head 18%, label 0.84, cap 0.12) give at least 5:1. The manual lists the variables. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tobias Kaestner --- doc/manual/reference/directives-and-roles.rst | 39 ++++++ sphinx/_static/zdocs-needs.css | 114 +++++++++++++----- 2 files changed, 124 insertions(+), 29 deletions(-) diff --git a/doc/manual/reference/directives-and-roles.rst b/doc/manual/reference/directives-and-roles.rst index a51fc09..3c91c2d 100644 --- a/doc/manual/reference/directives-and-roles.rst +++ b/doc/manual/reference/directives-and-roles.rst @@ -559,6 +559,45 @@ The ``need_cards`` extension (loaded by ``zdocs_conf``) gives each card the class ``zd-status-`` / ``zd-verdict-`` that colors the pill. A type without a color stays grey. +**The colors of a card are CSS variables.** ``zdocs-needs.css`` sets them on +``:root``, and a theme changes the card with these variables only. Set them +in a CSS file of the document that loads after ``zdocs-needs.css`` (the +``css_files`` of ``configure()``): + +.. code-block:: css + + :root { + --zd-card-bg: #fdfdfd; + --zd-card-fields: #f6f7f8; + } + +``--zd-card-bg``, ``--zd-card-fields`` + The card, and the row of its fields and links. +``--zd-card-head`` + The share of the type color in the head (a percentage). +``--zd-accent-l``, ``--zd-accent-c`` + The lightness (OKLCH) of the type label, and the most chroma that it can + have. The label is the type color with four times its chroma, up to + ``--zd-accent-c``. +``--zd-border``, ``--zd-rule``, ``--zd-label``, ``--zd-text``, ``--zd-id`` + The border, the lines in the card, the field labels, the text and the ID. +``--zd-shadow``, ``--zd-preview-shadow`` + The shadow of the card and of the preview popup. +``--zd-ok-*``, ``--zd-bad-*``, ``--zd-gap-*``, ``--zd-off-*`` + Each with ``-text``, ``-bg`` and ``-line``: the pills and the adequacy + chips for good, failing, a gap, and not judged or not run. + +The card has dark values too. They apply when ```` has +``data-theme="dark"`` or the class ``dark-mode``, the usual switches of a +dark mode for ``sphinx_rtd_theme``. A theme with another switch sets the +variables under its own selector. There is no ``prefers-color-scheme`` rule, +because ``sphinx_rtd_theme`` has no dark mode of its own. + +With the default values, the type label and the title have a contrast of at +least 4.5:1 (WCAG AA) on the head for any type color, in both themes. If you +change ``--zd-card-head`` or ``--zd-accent-*``, check the contrast of your +type colors again. + ``need_cards`` also adds ``zdocs-need-preview.js`` to each page. When the pointer stays on a link in a card, and the link goes to another need card, a popup shows a copy of that card: the head, then the fields, then the text. The diff --git a/sphinx/_static/zdocs-needs.css b/sphinx/_static/zdocs-needs.css index dea4a26..d8d77f9 100644 --- a/sphinx/_static/zdocs-needs.css +++ b/sphinx/_static/zdocs-needs.css @@ -16,25 +16,81 @@ * * Each need type has its color in --zd-type. The need_cards extension * sets it from the "color" of the type in the needs configuration - * (zdocs-need-types.css). The other colors come from it. + * (zdocs-need-types.css). The head and the type label come from it. */ -table.need.need.need[class*=" needs_layout_zdocs"] { - --zd-type: #eeeeee; - --zd-accent: #666666; - --zd-accent: oklch(from var(--zd-type) 0.52 calc(c * 4 + 0.02) h); +/* + * Every other color of the card is a variable. A theme changes the card + * with these variables only: it sets them on :root (or on html.), in + * a CSS file that loads after this one. + * + * The dark values apply when has data-theme="dark" or the class + * dark-mode, the two usual switches of a dark mode for sphinx_rtd_theme. + * There is no prefers-color-scheme rule: sphinx_rtd_theme itself has no dark + * mode, and a dark card on a light page is worse than a light card. + * + * The type label (--zd-accent) is the type color at the lightness + * --zd-accent-l, with at most the chroma --zd-accent-c: four times the + * chroma of the type, so that a pastel type gets a clear label. The cap keeps + * a saturated type in the sRGB gamut. The head mixes --zd-card-head of the + * type color into the card. With the values below, the label and the title + * have a contrast of at least 4.5:1 (WCAG AA) on the head for any type + * color, in both themes; for the pastel and the mid colors of a usual + * needs palette, at least 5:1. A theme that changes these values must check + * the contrast again. + */ +:root { + --zd-card-bg: #ffffff; + --zd-card-fields: #fafbfc; /* the row of the fields and links */ + --zd-card-head: 35%; /* the share of the type color in the head */ + --zd-accent-l: 0.34; /* the lightness of the type label (OKLCH) */ + --zd-accent-c: 0.14; /* the most chroma that the label can have */ --zd-border: #d5d9de; + --zd-rule: #e6e9ec; /* the lines in the card */ --zd-label: #5f6670; --zd-text: #1d2329; + --zd-id: #2b3a4a; + --zd-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + --zd-preview-shadow: 0 6px 24px rgba(0, 0, 0, 0.18), 0 1px 4px rgba(0, 0, 0, 0.12); + /* The pills and the adequacy chips: good, failing, a gap, not judged */ + --zd-ok-text: #116329; --zd-ok-bg: #dafbe1; --zd-ok-line: #1a7f37; + --zd-bad-text: #a40e26; --zd-bad-bg: #ffebe9; --zd-bad-line: #cf222e; + --zd-gap-text: #7d4e00; --zd-gap-bg: #fff8c5; --zd-gap-line: #bf8700; + --zd-off-text: #424a53; --zd-off-bg: #eaeef2; --zd-off-line: #8c959f; +} +html[data-theme="dark"], +html.dark-mode { + --zd-card-bg: #26272a; + --zd-card-fields: #222326; + --zd-card-head: 18%; + --zd-accent-l: 0.84; + --zd-accent-c: 0.12; + --zd-border: #3a3b3e; + --zd-rule: #34353a; + --zd-label: #a3a3a3; + --zd-text: #d6d6d6; + --zd-id: #f2f2f2; + --zd-shadow: none; + --zd-preview-shadow: 0 6px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--zd-border); + --zd-ok-text: #7ee787; --zd-ok-bg: rgba(46, 160, 67, 0.15); --zd-ok-line: #3fb950; + --zd-bad-text: #ffa198; --zd-bad-bg: rgba(248, 81, 73, 0.15); --zd-bad-line: #f85149; + --zd-gap-text: #e3b341; --zd-gap-bg: rgba(187, 128, 0, 0.15); --zd-gap-line: #d29922; + --zd-off-text: #c9d1d9; --zd-off-bg: rgba(110, 118, 129, 0.2); --zd-off-line: #8b949e; +} + +table.need.need.need[class*=" needs_layout_zdocs"] { + --zd-type: #eeeeee; + --zd-accent: #666666; + --zd-accent: oklch(from var(--zd-type) var(--zd-accent-l) min(calc(c * 4 + 0.02), var(--zd-accent-c)) h); --zd-mono: SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; display: block; margin: 1.5em 0; - background: #fff; + background: var(--zd-card-bg); border: 1px solid var(--zd-border); border-left: 5px solid var(--zd-accent); border-radius: 6px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + box-shadow: var(--zd-shadow); overflow: hidden; } @@ -67,7 +123,7 @@ table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.meta { order: 2; /* -- Head: type, ID and status; then the title ------------------------------ */ table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.head { - background: color-mix(in srgb, var(--zd-type) 55%, #fff); + background: color-mix(in srgb, var(--zd-type) var(--zd-card-head), var(--zd-card-bg)); } table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.head > td:not([rowspan]) { padding: 0.7em 1.25em 0.75em; @@ -82,7 +138,7 @@ table.need.need.need[class*=" needs_layout_zdocs"] tr.head .needs_type_name { text-transform: uppercase; } table.need.need.need[class*=" needs_layout_zdocs"] tr.head .needs-id a { - color: #2b3a4a; + color: var(--zd-id); font-family: var(--zd-mono); font-size: 0.85em; } @@ -105,25 +161,25 @@ table.need.need.need[class*=" needs_layout_zdocs"] tr.head :is(.needs_status, .n * statuses, like the "Draft" of a requirement, keep the type accent. */ table.need.need.need[class*=" needs_layout_zdocs"]:is(.zd-status-passed, .zd-verdict-true) tr.head :is(.needs_status, .needs_verdict) { - border-color: #1a7f37; - background: #dafbe1; - color: #116329; + border-color: var(--zd-ok-line); + background: var(--zd-ok-bg); + color: var(--zd-ok-text); } table.need.need.need[class*=" needs_layout_zdocs"]:is(.zd-status-failed, .zd-status-error, .zd-verdict-broken) tr.head :is(.needs_status, .needs_verdict) { - border-color: #cf222e; - background: #ffebe9; - color: #a40e26; + border-color: var(--zd-bad-line); + background: var(--zd-bad-bg); + color: var(--zd-bad-text); } table.need.need.need[class*=" needs_layout_zdocs"]:is(.zd-status-skipped, .zd-status-filtered, .zd-status-blocked, .zd-status-not-run, .zd-verdict-unattributed, .zd-verdict-unresolved, .zd-verdict-no-cov) tr.head :is(.needs_status, .needs_verdict) { - border-color: #8c959f; - background: #eaeef2; - color: #424a53; + border-color: var(--zd-off-line); + background: var(--zd-off-bg); + color: var(--zd-off-text); } /* A gap, not a failure: some of the code, or no code at all. */ table.need.need.need[class*=" needs_layout_zdocs"]:is(.zd-verdict-partial, .zd-verdict-no-impl) tr.head :is(.needs_status, .needs_verdict) { - border-color: #bf8700; - background: #fff8c5; - color: #7d4e00; + border-color: var(--zd-gap-line); + background: var(--zd-gap-bg); + color: var(--zd-gap-text); } table.need.need.need[class*=" needs_layout_zdocs"] tr.head .needs_title { display: block; @@ -224,7 +280,7 @@ html.writer-html5 .rst-content table.need.need.need[class*=" needs_layout_zdocs" html .rst-content table.need.need.need[class*=" needs_layout_zdocs"] div.need-refs { margin: 1rem 0 0.25rem; padding-top: 0.6rem; - border-top: 1px dashed #e1e4e8; + border-top: 1px dashed var(--zd-rule); } html.writer-html5 .rst-content table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.content > td div.need-refs > p { margin: 0.15rem 0; @@ -244,13 +300,13 @@ html .rst-content table.need.need.need[class*=" needs_layout_zdocs"] div.need-re /* -- Fields and links: one label/value grid ------------------------------------ */ html .rst-content table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.meta > td { - background: #fafbfc; + background: var(--zd-card-fields); font-size: 0.9rem; line-height: 1.6; } table.need.need.need[class*=" needs_layout_zdocs"] > tbody > tr.meta > td:not([rowspan]) { padding: 0.5rem 1.25rem 0.6rem; - border-top: 1px solid #e6e9ec; + border-top: 1px solid var(--zd-rule); border-bottom: none; } table.need.need.need[class*=" needs_layout_zdocs"] tr.meta div.line-block { @@ -373,9 +429,9 @@ table.need.need.need[class*=" needs_layout_zdocs"] :is(.zd-adq-ok, .zd-adq-miss, font-size: 0.9em; white-space: nowrap; } -table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-ok { background: #e3f3e6; color: #1e6b33; } -table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-miss { background: #fbe3e3; color: #a12828; } -table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-none { background: #eceef1; color: #5f6670; } +table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-ok { background: var(--zd-ok-bg); color: var(--zd-ok-text); } +table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-miss { background: var(--zd-bad-bg); color: var(--zd-bad-text); } +table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-none { background: var(--zd-off-bg); color: var(--zd-off-text); } /* -- Preview: the popup of zdocs-need-preview.js ----------------------------- */ @@ -390,9 +446,9 @@ table.need.need.need[class*=" needs_layout_zdocs"] .zd-adq-none { background: #e max-height: min(420px, 60vh); overflow-y: auto; overscroll-behavior: contain; - background: #fff; + background: var(--zd-card-bg); border-radius: 6px; - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18), 0 1px 4px rgba(0, 0, 0, 0.12); + box-shadow: var(--zd-preview-shadow); } .zd-preview[hidden] { display: none;