From cf2bc7302e8816d04688e96144a197e4a471859c Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 13:07:20 +0100 Subject: [PATCH 1/7] Teach the landing-page gate to read cards Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- tests/test_docs_landing_pages.py | 281 ++++++++++++++++++++++++++----- 1 file changed, 240 insertions(+), 41 deletions(-) diff --git a/tests/test_docs_landing_pages.py b/tests/test_docs_landing_pages.py index 4168486..eeb601d 100644 --- a/tests/test_docs_landing_pages.py +++ b/tests/test_docs_landing_pages.py @@ -2,17 +2,21 @@ # # This file is part of tephpy and is distributed under the 3-Clause BSD license. # See the LICENSE file in the package root directory for licensing details. -"""A landing page's table and its toctree are one list (narrative spec §3.9).""" +"""A landing page's index and its toctree are one list (narrative spec §3.9).""" from __future__ import annotations -from pathlib import Path +from functools import cache +from pathlib import Path, PurePosixPath import re import pytest +from tests.by_path import load_path + REPO = Path(__file__).parents[1] DOCS = REPO / "docs" / "src" +CONF = DOCS / "conf.py" #: The sections whose landing page carries a table (narrative spec §3.9). #: @@ -25,12 +29,20 @@ #: appearance would put the developer guide inside two gates that deliberately exclude #: it. #: -#: The reference quadrant is out, decided rather than deferred: its entries are reached -#: by name rather than chosen between, its introduction already guides a reader to the -#: two pages that need it, and it carries no prose list tracking a directory -- which is -#: the defect narrative spec §3.9 exists to close. +#: The reference quadrant takes cards instead (`CARD_SECTIONS`): its pages are +#: looked up by name, and a card answers that with an icon where a row would offer a +#: choice nobody makes (narrative spec §3.9). TABLE_SECTIONS = ("start", "tutorials", "howtos", "explanation", "developer") +#: The sections whose landing page carries a grid of cards (narrative spec §3.9). +#: Empty until the reference page takes the shape: every check below reads the live +#: page, so a section joins in the commit that gives it the cards. +CARD_SECTIONS: tuple[str, ...] = () + +#: The directive a card is written with. A card's own options are the ``:name:`` +#: lines directly under it, and the first line that is not one ends them. +CARD = ".. grid-item-card::" + #: A ``:doc:`` role, with the explicit target that wins over the display text when #: one is written -- the same two-part shape ``check_glossary_links.py`` reads a #: ``:term:`` in. @@ -109,22 +121,73 @@ def toctree_options(source: str) -> list[str]: ] -def _entries(directory: Path) -> list[Path]: +@cache +def _autoapi() -> tuple[PurePosixPath, str]: + """Return where autoapi writes the API pages, and the package it documents. + + Read by executing `conf.py`, as `tests/test_docs_whatsnew.py` reads + ``rst_epilog``: a text scan would pass on a value sitting in a comment. Cached, + because every section's discovery asks and the answer cannot change in a run. + + Returns + ------- + tuple of (PurePosixPath, str) + ``autoapi_root``, relative to the documentation source, and the name of the + package directory ``autoapi_dirs`` names. + + """ + conf = load_path("tephpy_docs_conf", CONF) + return PurePosixPath(conf.autoapi_root), PurePosixPath(conf.autoapi_dirs[0]).name + + +def generated_pages(section: str) -> list[str]: + """Return the pages a section offers that exist only while a build runs. + + autoapi writes the API reference under ``autoapi_root`` during a build and, with + ``autoapi_keep_files = False``, removes it afterwards; the directory is + git-ignored besides. Discovery cannot see a page that is not on disk, so the one + entry a section's index gives the API is derived instead (narrative spec §3.9). + + Parameters + ---------- + section : str + The section's directory name under the documentation source. + + Returns + ------- + list of str + ``["generated/api/tephpy/index"]`` for the section ``autoapi_root`` sits in, + and nothing for any other. + + """ + root, package = _autoapi() + if root.parts[0] != section: + return [] + return [str(PurePosixPath(*root.parts[1:], package, "index"))] + + +def _entries(directory: Path, skip: Path) -> list[Path]: """Return the documents one section offers, one path per destination. A subdirectory carrying its own ``index.rst`` is a subsection: it contributes that landing page and **nothing beneath it**, because from the - parent's table it is a single destination however many documents sit inside + parent's index it is a single destination however many documents sit inside it -- the specification collection is one row, not twenty. A subdirectory without one is a plain grouping, and its documents belong to the parent. Recursion stops at a landing page rather than pruning by name, so a subsection nested two deep behaves the same as one nested one deep (narrative spec §3.9). + ``skip`` is autoapi's output directory, passed over wherever a build left it: + its one page is counted by `generated_pages` instead, and a stale tree would + otherwise add entries no index names. + Parameters ---------- directory : Path The section directory to read. + skip : Path + The directory autoapi writes into. Returns ------- @@ -134,41 +197,46 @@ def _entries(directory: Path) -> list[Path]: """ found: list[Path] = [] for path in directory.iterdir(): + if path == skip: + continue if path.is_dir(): landing_page = path / "index.rst" if landing_page.is_file(): found.append(landing_page) else: - found.extend(_entries(path)) + found.extend(_entries(path, skip)) elif path.suffix == ".rst" and path.name != "index.rst": found.append(path) return found def pages(quadrant: str, docs: Path = DOCS) -> list[str]: - """Return every page in a quadrant, as a landing table would name it. + """Return every page on disk in a section, as its landing index would name it. - A `:doc:` target on a landing page is relative to the quadrant, so that is + A `:doc:` target on a landing page is relative to the section, so that is what these are made relative to. The section's own ``index.rst`` is a landing page rather than an entry in one and is left out; a subsection's is both, and - counts as one entry of its parent. + counts as one entry of its parent. A page only a build writes is not on disk, + and `generated_pages` supplies it. Parameters ---------- quadrant : str - The quadrant's directory name under ``docs``. + The section's directory name under ``docs``. docs : Path, optional The documentation source root. Returns ------- list of str - The quadrant's pages, sorted. + The section's pages, sorted. """ root = docs / quadrant + skip = docs / _autoapi()[0] return sorted( - path.relative_to(root).with_suffix("").as_posix() for path in _entries(root) + path.relative_to(root).with_suffix("").as_posix() + for path in _entries(root, skip) ) @@ -202,6 +270,62 @@ def table_targets(source: str) -> list[str | None]: return found +def card_targets(source: str) -> list[str | None]: + """Return the documents a page's landing cards link to, in card order. + + Only a card's own option block is read -- the ``:name: value`` lines directly + under ``.. grid-item-card::`` -- so an option of an image nested in the card, or + a field in its body, is not mistaken for the card's link. + + Parameters + ---------- + source : str + The reStructuredText source of one page. + + Returns + ------- + list of str or None + Each card's ``:link:``, or ``None`` for a card carrying none -- reported + rather than skipped, so a card that links nowhere fails the page instead of + shrinking the list silently. + + """ + lines = source.splitlines() + found: list[str | None] = [] + for index, line in enumerate(lines): + if not line.strip().startswith(CARD): + continue + link = None + for option in lines[index + 1 :]: + stripped = option.strip() + if not stripped.startswith(":"): + break + name, _, value = stripped[1:].partition(":") + if name == "link": + link = value.strip() + found.append(link) + return found + + +def index_targets(section: str, source: str) -> list[str | None]: + """Return a landing page's index, read in the shape its section takes. + + Parameters + ---------- + section : str + The section's directory name. + source : str + The reStructuredText source of its landing page. + + Returns + ------- + list of str or None + What `card_targets` reads for a card section, and `table_targets` otherwise. + + """ + return card_targets(source) if section in CARD_SECTIONS else table_targets(source) + + def landing(quadrant: str, docs: Path = DOCS) -> str: """Return one quadrant's landing page source. @@ -310,56 +434,131 @@ def test_a_subdirectory_without_a_landing_page_gives_up_its_documents(tmp_path): assert pages("howtos", docs=tmp_path) == ["advanced/tuning"] +def test_card_targets_reads_each_cards_link_in_order(): + source = ( + " .. grid-item-card:: API\n" + " :link: generated/api/tephpy/index\n" + " :link-type: doc\n" + " :columns: 12\n\n" + " Generated.\n\n" + " .. grid-item-card:: Command Line\n" + " :link-type: doc\n" + " :link: cli\n\n" + " What to type.\n" + ) + assert card_targets(source) == ["generated/api/tephpy/index", "cli"] + + +def test_card_targets_reports_a_card_that_links_nowhere(): + """Reported rather than skipped, as a table row is.""" + source = ( + " .. grid-item-card:: Glossary\n" + " :class-card: teph-card sd-rounded-3\n\n" + " .. image:: glossary-light.svg\n" + " :class: only-light teph-card-icon\n" + ) + assert card_targets(source) == [None] + + +def test_card_targets_reads_only_the_cards_own_options(): + """A ``:link:`` after the option block is body text, not the card's link.""" + source = ( + " .. grid-item-card:: Command Line\n" + " :link-type: doc\n\n" + " :link: cli\n" + ) + assert card_targets(source) == [None] + + +def test_the_api_page_is_derived_for_the_section_autoapi_writes_into(): + """It exists only while a build runs, so discovery cannot find it.""" + assert generated_pages("reference") == ["generated/api/tephpy/index"] + assert generated_pages("howtos") == [] + + +def test_discovery_passes_over_a_generated_tree_a_build_left_behind(tmp_path): + """A stale autoapi tree must not add pages the index is then asked to list.""" + section = tmp_path / "reference" + (section / "generated" / "api" / "tephpy").mkdir(parents=True) + (section / "index.rst").touch() + (section / "cli.rst").touch() + (section / "generated" / "api" / "index.rst").touch() + (section / "generated" / "api" / "tephpy" / "index.rst").touch() + assert pages("reference", docs=tmp_path) == ["cli"] + + def test_every_section_this_gate_governs_is_on_disk(): """A gate that finds nothing passes by never having looked.""" - for quadrant in TABLE_SECTIONS: - assert (DOCS / quadrant).is_dir(), f"{quadrant} is missing" + for section in TABLE_SECTIONS + CARD_SECTIONS: + assert (DOCS / section).is_dir(), f"{section} is missing" + + +def test_a_section_takes_one_shape(): + """Two constants naming one section would put two indexes on its page.""" + assert not set(TABLE_SECTIONS) & set(CARD_SECTIONS) -@pytest.mark.parametrize("quadrant", TABLE_SECTIONS) -def test_the_table_and_the_toctree_are_one_ordered_list(quadrant): +@pytest.mark.parametrize("section", TABLE_SECTIONS + CARD_SECTIONS) +def test_a_landing_page_carries_one_index(section): + """A table page carries no cards, and a card page no table (narrative spec §3.9).""" + other = ".. list-table::" if section in CARD_SECTIONS else CARD + assert other not in landing(section) + + +@pytest.mark.parametrize("section", TABLE_SECTIONS + CARD_SECTIONS) +def test_the_index_and_the_toctree_are_one_ordered_list(section): """Narrative spec §3.9: the visible index and the navigation are one list. Sequence and not set. The toctree is hidden, which hides it from the page body and from nothing else: the sidebar, the breadcrumb and the previous/next footer - all read its order, so a table ordered differently would disagree with the + all read its order, so an index ordered differently would disagree with the navigation drawn around it. """ - source = landing(quadrant) - assert table_targets(source) == toctree_entries(source) + source = landing(section) + assert index_targets(section, source) == toctree_entries(source) + +@pytest.mark.parametrize("section", TABLE_SECTIONS + CARD_SECTIONS) +def test_every_entry_links_to_a_page_in_its_own_section(section): + """A target is a page of the section, or the one page a build generates there. -@pytest.mark.parametrize("quadrant", TABLE_SECTIONS) -def test_every_row_links_to_a_page_in_its_own_quadrant(quadrant): - for target in table_targets(landing(quadrant)): - assert target is not None, ( - f"{quadrant} has a row whose first cell links nowhere" + ``..`` is refused outright: ``DOCS / section / "../howtos/units.rst"`` names a + file that exists, so without it an entry pointing into another section would + pass as a page of this one. + """ + generated = generated_pages(section) + for target in index_targets(section, landing(section)): + assert target is not None, f"{section} has an entry that links nowhere" + assert ".." not in PurePosixPath(target).parts, ( + f"{section}'s index links to {target}, outside the section" ) - assert (DOCS / quadrant / f"{target}.rst").is_file(), ( - f"{quadrant}'s table links to {target}, which is not a page in it" + assert target in generated or (DOCS / section / f"{target}.rst").is_file(), ( + f"{section}'s index links to {target}, which is not a page in it" ) -@pytest.mark.parametrize("quadrant", TABLE_SECTIONS) -def test_the_table_lists_every_page_in_the_quadrant(quadrant): - """The table is the quadrant's index, so it indexes the quadrant. +@pytest.mark.parametrize("section", TABLE_SECTIONS + CARD_SECTIONS) +def test_the_index_lists_every_page_in_the_section(section): + """The index is the section's index, so it indexes the section. - The ordered comparison above holds the table and the toctree to each other and + The ordered comparison above holds the index and the toctree to each other and would not notice a page missing from both, which is how a page goes unlisted: one commit that adds a page and neither list. The fail-on-warning build catches the ordinary case -- Sphinx reports a document in no toctree -- but not an - `:orphan:` page, which builds clean and would sit in the quadrant unreachable + `:orphan:` page, which builds clean and would sit in the section unreachable from its own landing page. """ - listed = sorted(target for target in table_targets(landing(quadrant)) if target) - assert listed == pages(quadrant) + listed = sorted( + target for target in index_targets(section, landing(section)) if target + ) + assert listed == sorted(pages(section) + generated_pages(section)) -@pytest.mark.parametrize("quadrant", TABLE_SECTIONS) -def test_the_toctree_is_hidden(quadrant): - """Narrative spec §3.9: the table is the visible index, and it is the only one. +@pytest.mark.parametrize("section", TABLE_SECTIONS + CARD_SECTIONS) +def test_the_toctree_is_hidden(section): + """Narrative spec §3.9: the index is the visible one, and it is the only one. - Without this the page renders the same list twice, the table and the toctree + Without this the page renders the same list twice, the index and the toctree under it, which is the duplication the shape exists to remove. """ - assert ":hidden:" in toctree_options(landing(quadrant)) + assert ":hidden:" in toctree_options(landing(section)) From 46fe98aecf561599b29ced18f949b4cfd520be11 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 13:26:58 +0100 Subject: [PATCH 2/7] Name the landing cards' classes for cards, not quadrants Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- docs/src/_static/tephpy.css | 6 +++--- docs/src/index.rst | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/_static/tephpy.css b/docs/src/_static/tephpy.css index a7ef4ae..1efb992 100644 --- a/docs/src/_static/tephpy.css +++ b/docs/src/_static/tephpy.css @@ -80,7 +80,7 @@ * a white background to every `.bd-content img` on top of that -- two rules of * different reach, and the class is the opt-out named in both. */ -.teph-quadrant-icon { +.teph-card-icon { display: block; margin: 0 0 0.6rem; width: 56px; @@ -98,12 +98,12 @@ * to it anyway, but nothing here has to know that. */ @media (min-width: 576px) { - .teph-quadrant .sd-card-body { + .teph-card .sd-card-body { padding-left: 84px; position: relative; } - .teph-quadrant-icon { + .teph-card-icon { left: 16px; margin: 0; position: absolute; diff --git a/docs/src/index.rst b/docs/src/index.rst index 94d6307..7a81400 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -26,14 +26,14 @@ Plot and analyse :term:`tephigrams `. .. grid-item-card:: Tutorials :link: tutorials/index :link-type: doc - :class-card: teph-quadrant sd-rounded-3 + :class-card: teph-card sd-rounded-3 .. image:: _static/cards/tutorials-light.svg - :class: only-light teph-quadrant-icon + :class: only-light teph-card-icon :alt: a path stepping up across the tephigram lattice to a marked end .. image:: _static/cards/tutorials-dark.svg - :class: only-dark teph-quadrant-icon + :class: only-dark teph-card-icon :alt: a path stepping up across the tephigram lattice to a marked end Learning-oriented lessons. @@ -41,14 +41,14 @@ Plot and analyse :term:`tephigrams `. .. grid-item-card:: How-To Guides :link: howtos/index :link-type: doc - :class-card: teph-quadrant sd-rounded-3 + :class-card: teph-card sd-rounded-3 .. image:: _static/cards/howtos-light.svg - :class: only-light teph-quadrant-icon + :class: only-light teph-card-icon :alt: one isopleth of a family drawn at a heavier weight .. image:: _static/cards/howtos-dark.svg - :class: only-dark teph-quadrant-icon + :class: only-dark teph-card-icon :alt: one isopleth of a family drawn at a heavier weight Goal-oriented recipes. @@ -56,14 +56,14 @@ Plot and analyse :term:`tephigrams `. .. grid-item-card:: Explanation :link: explanation/index :link-type: doc - :class-card: teph-quadrant sd-rounded-3 + :class-card: teph-card sd-rounded-3 .. image:: _static/cards/explanation-light.svg - :class: only-light teph-quadrant-icon + :class: only-light teph-card-icon :alt: a pair of axes turning through 45 degrees .. image:: _static/cards/explanation-dark.svg - :class: only-dark teph-quadrant-icon + :class: only-dark teph-card-icon :alt: a pair of axes turning through 45 degrees Understanding-oriented background. @@ -71,14 +71,14 @@ Plot and analyse :term:`tephigrams `. .. grid-item-card:: Reference :link: reference/index :link-type: doc - :class-card: teph-quadrant sd-rounded-3 + :class-card: teph-card sd-rounded-3 .. image:: _static/cards/reference-light.svg - :class: only-light teph-quadrant-icon + :class: only-light teph-card-icon :alt: an index of entries, one of them marked .. image:: _static/cards/reference-dark.svg - :class: only-dark teph-quadrant-icon + :class: only-dark teph-card-icon :alt: an index of entries, one of them marked Information-oriented API and glossary. From c4bc9a52bf27d91e4aba26c5414699591a3f4eb0 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 13:34:11 +0100 Subject: [PATCH 3/7] Draw the reference cards' icons, and knock out the Tutorials halo Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- docs/src/_static/cards/reference/api-dark.svg | 18 +++++++++++++++++ .../src/_static/cards/reference/api-light.svg | 18 +++++++++++++++++ .../cards/reference/changelog-dark.svg | 20 +++++++++++++++++++ .../cards/reference/changelog-light.svg | 20 +++++++++++++++++++ docs/src/_static/cards/reference/cli-dark.svg | 15 ++++++++++++++ .../src/_static/cards/reference/cli-light.svg | 15 ++++++++++++++ .../_static/cards/reference/config-dark.svg | 18 +++++++++++++++++ .../_static/cards/reference/config-light.svg | 18 +++++++++++++++++ .../_static/cards/reference/glossary-dark.svg | 16 +++++++++++++++ .../cards/reference/glossary-light.svg | 16 +++++++++++++++ .../cards/reference/references-dark.svg | 16 +++++++++++++++ .../cards/reference/references-light.svg | 16 +++++++++++++++ .../_static/cards/reference/whatsnew-dark.svg | 15 ++++++++++++++ .../cards/reference/whatsnew-light.svg | 15 ++++++++++++++ docs/src/_static/cards/tutorials-dark.svg | 2 +- 15 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 docs/src/_static/cards/reference/api-dark.svg create mode 100644 docs/src/_static/cards/reference/api-light.svg create mode 100644 docs/src/_static/cards/reference/changelog-dark.svg create mode 100644 docs/src/_static/cards/reference/changelog-light.svg create mode 100644 docs/src/_static/cards/reference/cli-dark.svg create mode 100644 docs/src/_static/cards/reference/cli-light.svg create mode 100644 docs/src/_static/cards/reference/config-dark.svg create mode 100644 docs/src/_static/cards/reference/config-light.svg create mode 100644 docs/src/_static/cards/reference/glossary-dark.svg create mode 100644 docs/src/_static/cards/reference/glossary-light.svg create mode 100644 docs/src/_static/cards/reference/references-dark.svg create mode 100644 docs/src/_static/cards/reference/references-light.svg create mode 100644 docs/src/_static/cards/reference/whatsnew-dark.svg create mode 100644 docs/src/_static/cards/reference/whatsnew-light.svg diff --git a/docs/src/_static/cards/reference/api-dark.svg b/docs/src/_static/cards/reference/api-dark.svg new file mode 100644 index 0000000..b9e0d6c --- /dev/null +++ b/docs/src/_static/cards/reference/api-dark.svg @@ -0,0 +1,18 @@ + + + a module tree, one entry found + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/api-light.svg b/docs/src/_static/cards/reference/api-light.svg new file mode 100644 index 0000000..40915ef --- /dev/null +++ b/docs/src/_static/cards/reference/api-light.svg @@ -0,0 +1,18 @@ + + + a module tree, one entry found + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/changelog-dark.svg b/docs/src/_static/cards/reference/changelog-dark.svg new file mode 100644 index 0000000..3191f70 --- /dev/null +++ b/docs/src/_static/cards/reference/changelog-dark.svg @@ -0,0 +1,20 @@ + + + a run of entries, the newest marked + + + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/changelog-light.svg b/docs/src/_static/cards/reference/changelog-light.svg new file mode 100644 index 0000000..9783dfa --- /dev/null +++ b/docs/src/_static/cards/reference/changelog-light.svg @@ -0,0 +1,20 @@ + + + a run of entries, the newest marked + + + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/cli-dark.svg b/docs/src/_static/cards/reference/cli-dark.svg new file mode 100644 index 0000000..f7ef205 --- /dev/null +++ b/docs/src/_static/cards/reference/cli-dark.svg @@ -0,0 +1,15 @@ + + + a shell prompt, its cursor marked + + + + + + + diff --git a/docs/src/_static/cards/reference/cli-light.svg b/docs/src/_static/cards/reference/cli-light.svg new file mode 100644 index 0000000..57b3cf5 --- /dev/null +++ b/docs/src/_static/cards/reference/cli-light.svg @@ -0,0 +1,15 @@ + + + a shell prompt, its cursor marked + + + + + + + diff --git a/docs/src/_static/cards/reference/config-dark.svg b/docs/src/_static/cards/reference/config-dark.svg new file mode 100644 index 0000000..e98fb84 --- /dev/null +++ b/docs/src/_static/cards/reference/config-dark.svg @@ -0,0 +1,18 @@ + + + two options on their scales, one of them set + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/config-light.svg b/docs/src/_static/cards/reference/config-light.svg new file mode 100644 index 0000000..7e8bad5 --- /dev/null +++ b/docs/src/_static/cards/reference/config-light.svg @@ -0,0 +1,18 @@ + + + two options on their scales, one of them set + + + + + + + + + + diff --git a/docs/src/_static/cards/reference/glossary-dark.svg b/docs/src/_static/cards/reference/glossary-dark.svg new file mode 100644 index 0000000..c228675 --- /dev/null +++ b/docs/src/_static/cards/reference/glossary-dark.svg @@ -0,0 +1,16 @@ + + + a line, and the label that names it + + + + + + + + diff --git a/docs/src/_static/cards/reference/glossary-light.svg b/docs/src/_static/cards/reference/glossary-light.svg new file mode 100644 index 0000000..3e7411e --- /dev/null +++ b/docs/src/_static/cards/reference/glossary-light.svg @@ -0,0 +1,16 @@ + + + a line, and the label that names it + + + + + + + + diff --git a/docs/src/_static/cards/reference/references-dark.svg b/docs/src/_static/cards/reference/references-dark.svg new file mode 100644 index 0000000..ba0354f --- /dev/null +++ b/docs/src/_static/cards/reference/references-dark.svg @@ -0,0 +1,16 @@ + + + a printed page, its source marked + + + + + + + + diff --git a/docs/src/_static/cards/reference/references-light.svg b/docs/src/_static/cards/reference/references-light.svg new file mode 100644 index 0000000..d18b4b7 --- /dev/null +++ b/docs/src/_static/cards/reference/references-light.svg @@ -0,0 +1,16 @@ + + + a printed page, its source marked + + + + + + + + diff --git a/docs/src/_static/cards/reference/whatsnew-dark.svg b/docs/src/_static/cards/reference/whatsnew-dark.svg new file mode 100644 index 0000000..e2437d5 --- /dev/null +++ b/docs/src/_static/cards/reference/whatsnew-dark.svg @@ -0,0 +1,15 @@ + + + a spark, marking what is new + + + + + + + diff --git a/docs/src/_static/cards/reference/whatsnew-light.svg b/docs/src/_static/cards/reference/whatsnew-light.svg new file mode 100644 index 0000000..9e85d15 --- /dev/null +++ b/docs/src/_static/cards/reference/whatsnew-light.svg @@ -0,0 +1,15 @@ + + + a spark, marking what is new + + + + + + + diff --git a/docs/src/_static/cards/tutorials-dark.svg b/docs/src/_static/cards/tutorials-dark.svg index c52e6f3..210c212 100644 --- a/docs/src/_static/cards/tutorials-dark.svg +++ b/docs/src/_static/cards/tutorials-dark.svg @@ -12,5 +12,5 @@ - + From bfb7b65e9e2ef2fa1f5dd540e9e1aa18860a3683 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 13:49:20 +0100 Subject: [PATCH 4/7] Give the reference quadrant a landing page of cards Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- docs/src/_static/tephpy.css | 10 ++- docs/src/reference/index.rst | 124 ++++++++++++++++++++++++++++++- tests/test_docs_landing_pages.py | 4 +- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/docs/src/_static/tephpy.css b/docs/src/_static/tephpy.css index 1efb992..76b79f4 100644 --- a/docs/src/_static/tephpy.css +++ b/docs/src/_static/tephpy.css @@ -66,7 +66,8 @@ } /* - * The four Diátaxis quadrant cards on the landing page. + * The landing pages' cards: the root page's four Diátaxis quadrants, and the + * reference quadrant's seven (narrative spec §3.9). * * The icon is a pair of images rather than sphinx-design's `:img-top:`, which * takes one path and so cannot carry a light and a dark drawing. The pair is @@ -88,7 +89,8 @@ /* * Beside the text once there is room for it, above the text when there is not. - * `.. grid:: 2` is two columns at *every* breakpoint -- the emitted row carries + * The root page's `.. grid:: 2` is two columns at *every* breakpoint -- the + * emitted row carries * `sd-row-cols-xs-2` through `sd-row-cols-lg-2` -- so a phone gets two cards of * about 150px, and an icon indented out of the flow left "Understanding- * oriented background." wrapping one word per line. Measured in Chromium at a @@ -96,6 +98,10 @@ * the narrow layout needs no override: an `only-light`/`only-dark` image is * hidden with `display: none !important` and any `display` this sets would lose * to it anyway, but nothing here has to know that. + * + * The reference quadrant's `.. grid:: 1 2 2 2` is one column below 576px, so its + * cards never meet that width. Above the icon, the root page's longest segment + * still splits at 360px (:issue:`328`). */ @media (min-width: 576px) { .teph-card .sd-card-body { diff --git a/docs/src/reference/index.rst b/docs/src/reference/index.rst index c314018..e9ff6c9 100644 --- a/docs/src/reference/index.rst +++ b/docs/src/reference/index.rst @@ -1,12 +1,127 @@ Reference ========= +.. The cards are the visible index and the toctree below is the navigation; the two + are one list, held by tests/test_docs_landing_pages.py (narrative spec §3.9). + + Each icon is drawn in the root page's vocabulary, as that page's comment sets it + out: API, a module tree with one entry found; Command Line, a shell prompt at its + cursor; Configuration Options, two options on their scales with one set; + Glossary, a line and the label that names it; References, a printed page with its + source marked; What's New, a spark; Changelog, a run of entries with the newest + marked. Light and dark differ only in the navy and the knock-out halo, and live in + _static/cards/reference/. + The factual material, for looking things up rather than reading through. -The API documentation is generated from the source, so it describes the package -you have installed. Beside it sit the command line, every configuration option and -its default, a glossary, the published sources this documentation cites, what's new -in each release, and the changelog. +.. grid:: 1 2 2 2 + :gutter: 2 + + .. grid-item-card:: API + :link: generated/api/tephpy/index + :link-type: doc + :class-card: teph-card sd-rounded-3 + :columns: 12 + + .. image:: ../_static/cards/reference/api-light.svg + :class: only-light teph-card-icon + :alt: a module tree, one entry found + + .. image:: ../_static/cards/reference/api-dark.svg + :class: only-dark teph-card-icon + :alt: a module tree, one entry found + + Generated from the source, so it describes the version you have installed. + + .. grid-item-card:: Command Line + :link: cli + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/cli-light.svg + :class: only-light teph-card-icon + :alt: a shell prompt, its cursor marked + + .. image:: ../_static/cards/reference/cli-dark.svg + :class: only-dark teph-card-icon + :alt: a shell prompt, its cursor marked + + What to type at a shell. + + .. grid-item-card:: Configuration Options + :link: config + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/config-light.svg + :class: only-light teph-card-icon + :alt: two options on their scales, one of them set + + .. image:: ../_static/cards/reference/config-dark.svg + :class: only-dark teph-card-icon + :alt: two options on their scales, one of them set + + What to set, in Python or in a file. + + .. grid-item-card:: Glossary + :link: glossary + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/glossary-light.svg + :class: only-light teph-card-icon + :alt: a line, and the label that names it + + .. image:: ../_static/cards/reference/glossary-dark.svg + :class: only-dark teph-card-icon + :alt: a line, and the label that names it + + What a word means here, and where the API carries it. + + .. grid-item-card:: References + :link: references + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/references-light.svg + :class: only-light teph-card-icon + :alt: a printed page, its source marked + + .. image:: ../_static/cards/reference/references-dark.svg + :class: only-dark teph-card-icon + :alt: a printed page, its source marked + + Where a convention or definition came from. + + .. grid-item-card:: What's New + :link: whatsnew/index + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/whatsnew-light.svg + :class: only-light teph-card-icon + :alt: a spark, marking what is new + + .. image:: ../_static/cards/reference/whatsnew-dark.svg + :class: only-dark teph-card-icon + :alt: a spark, marking what is new + + A release, in the few things worth knowing. + + .. grid-item-card:: Changelog + :link: changelog + :link-type: doc + :class-card: teph-card sd-rounded-3 + + .. image:: ../_static/cards/reference/changelog-light.svg + :class: only-light teph-card-icon + :alt: a run of entries, the newest marked + + .. image:: ../_static/cards/reference/changelog-dark.svg + :class: only-dark teph-card-icon + :alt: a run of entries, the newest marked + + A release, one entry per pull request. If you are deciding what to catch, read :mod:`tephpy.exceptions`. What ``tephpy`` raises about your data — its units, its physical consistency, the @@ -22,6 +137,7 @@ concept in one plain sentence and then says how it appears in the package — th data it involves, its units, and the API that carries it. .. toctree:: + :hidden: :maxdepth: 1 generated/api/tephpy/index diff --git a/tests/test_docs_landing_pages.py b/tests/test_docs_landing_pages.py index eeb601d..c5ab164 100644 --- a/tests/test_docs_landing_pages.py +++ b/tests/test_docs_landing_pages.py @@ -35,9 +35,7 @@ TABLE_SECTIONS = ("start", "tutorials", "howtos", "explanation", "developer") #: The sections whose landing page carries a grid of cards (narrative spec §3.9). -#: Empty until the reference page takes the shape: every check below reads the live -#: page, so a section joins in the commit that gives it the cards. -CARD_SECTIONS: tuple[str, ...] = () +CARD_SECTIONS: tuple[str, ...] = ("reference",) #: The directive a card is written with. A card's own options are the ``:name:`` #: lines directly under it, and the first line that is not one ends them. From c070890aa0dfe46877ec8ba71a09a43f99a5252f Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 15:15:13 +0100 Subject: [PATCH 5/7] Write the card shape into the landing-page rule Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- docs/src/developer/docs-style.rst | 62 +++++++++++++++++++------------ tests/test_docs_snippets.py | 9 ++--- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/docs/src/developer/docs-style.rst b/docs/src/developer/docs-style.rst index 9661a00..3a924b9 100644 --- a/docs/src/developer/docs-style.rst +++ b/docs/src/developer/docs-style.rst @@ -439,16 +439,17 @@ or adding the reason. Landing Pages ------------- -A section landing page — each of the four Diátaxis quadrants, and -:doc:`getting started <../start/index>` — is navigation rather than prose, which is -the rule above read forwards: it carries no reading-time banner because nobody reads -it through. -It carries, in order, an introduction, one two-column ``list-table``, and a hidden -``toctree``. - -The introduction says what the quadrant is for, who it assumes the reader is, what -it guarantees of every page in it, and where to go if this is the wrong quadrant. -It says nothing about an individual page. A paragraph that summarises the quadrant +A section landing page is navigation rather than prose, which is the rule above read +forwards: it carries no reading-time banner because nobody reads it through. Six +sections take one of two shapes, and both end in a hidden ``toctree``. + +**A table**, in :doc:`getting started <../start/index>`, the tutorials, how-to and +explanation quadrants, and this developer guide. The page carries, in order, an +introduction, one two-column ``list-table``, and the ``toctree``. + +The introduction says what the section is for, who it assumes the reader is, what +it guarantees of every page in it, and where to go if this is the wrong section. +It says nothing about an individual page. A paragraph that summarises the section page by page is a list that has to track a directory, and the how-to page's grew from six clauses to nine by hand before this rule existed. @@ -458,23 +459,36 @@ it is deliberately not the page's opening line, which a hover already shows. Wri ``:widths: auto`` and no header row, which is the shape the API reference's own summary tables already take. -The rows and the toctree carry the same pages in the same order, and it is the +**A grid of cards**, in the reference quadrant, whose pages are looked up by name +rather than chosen between. The page carries, in order, a one-sentence +introduction, a ``.. grid:: 1 2 2 2`` of cards, the guidance paragraphs, and the +``toctree``. The API card comes first and spans the row with ``:columns: 12``. One +column below 576px is not a matter of taste: two columns at that width split words +mid-word, which narrative spec §3.9 records measuring. + +Each card takes its page's title — the API card excepted, because that page is +titled by the package name — a light and a dark icon from +``_static/cards/reference/``, and one sentence that tells it from the card beside +it. A card raises no hover tooltip, so its sentence may say what the page opens +with where that is clearest. Draw an icon in the root page's vocabulary; its dark +file differs from the light one only in the navy, ``#8FB8E8`` for ``#1B3A6B``, and +the knock-out halo, ``#14181e`` for ``#FFFFFF``. + +The index and the toctree carry the same pages in the same order, and it is the order a reader needs rather than the alphabet. Hiding a toctree hides it from the page body only: the sidebar, the breadcrumb and the previous and next footer all read its order. ``tests/test_docs_landing_pages.py`` fails when the two disagree, -in membership or in order; when the table omits a page the quadrant holds, which -an ``:orphan:`` page would otherwise do silently, since the build's own -toctree check never sees one; and when the toctree is not hidden, which would -publish the same list twice. - -Glossary terms stay out of the cells. A table is a directive, and :ref:`the -first-mention rule ` already passes over a directive's body, so a -``:term:`` in a cell neither satisfies that rule nor breaks it. Write first -mentions in the introduction, and let a cell take the plain word. - -The reference quadrant is outside this rule for now: its entries are reached by -name rather than chosen between, and ``narrative spec §3.9`` records the question -rather than answering it. +in membership or in order; when an entry links outside its section; when the index +omits a page the section holds, which an ``:orphan:`` page would otherwise do +silently, since the build's own toctree check never sees one; and when the toctree +is not hidden, which would publish the same list twice. It reads a row's first +cell and a card's ``:link:``, and derives the API card's page from ``conf.py``, +since that page exists only while a build runs. + +Glossary terms stay out of the cells and the cards. Both are directives, and +:ref:`the first-mention rule ` already passes over a directive's +body, so a ``:term:`` in either neither satisfies that rule nor breaks it. Write +first mentions in the prose, and let a cell or a card take the plain word. Topic Tags ---------- diff --git a/tests/test_docs_snippets.py b/tests/test_docs_snippets.py index 20a8b6f..18043c4 100644 --- a/tests/test_docs_snippets.py +++ b/tests/test_docs_snippets.py @@ -1170,12 +1170,11 @@ def test_a_page_naming_an_exception_points_at_the_hierarchy(): def test_the_reference_index_sends_a_caller_to_the_hierarchy(): - """The quadrant's own description otherwise omits the subject entirely. + """The quadrant's own landing page otherwise omits the subject entirely. - The index enumerates what the reference holds — the command line, the - configuration options, the glossary, the citations, the changelog — and - named no exception at all, so a reader deciding what to catch had nothing - to follow (:issue:`213`). + Its cards name what the reference holds, and none of them is the exception + hierarchy, so without this paragraph a reader deciding what to catch has + nothing to follow (:issue:`213`). """ index = (DOCS / "reference" / "index.rst").read_text(encoding="utf-8") assert SIGNPOSTS in index, ( From bd69593cff2e143ec26b3cfaa332361300dbfd7c Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 15:41:16 +0100 Subject: [PATCH 6/7] Correct the plan's mutation restores in Task 4 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- .../2026-09-15-tephpy-reference-cards.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/src/developer/plans/2026-09-15-tephpy-reference-cards.md b/docs/src/developer/plans/2026-09-15-tephpy-reference-cards.md index 9ded65e..ebc1386 100644 --- a/docs/src/developer/plans/2026-09-15-tephpy-reference-cards.md +++ b/docs/src/developer/plans/2026-09-15-tephpy-reference-cards.md @@ -984,13 +984,22 @@ Expected: PASS. - [ ] **Step 5: Prove each check by mutation** +*Corrected 2026-09-15, during implementation:* the restores below originally used ``git checkout --``, which restores the page this task replaces. + Run each block from a clean tree, read the failure, and restore before the next. +```bash +# Step 3's page is not committed until Step 8, so `git checkout --` would restore +# the page this task replaces. Restore from this copy instead. +cp docs/src/reference/index.rst "${TMPDIR:-/tmp}/reference-index.rst" +``` + ```bash # (a) order: swap What's New and Changelog in the toctree only sed -i -e 's|^ whatsnew/index$| SWAP|' -e 's|^ changelog$| whatsnew/index|' -e 's|^ SWAP$| changelog|' docs/src/reference/index.rst pixi run -e test pytest tests/test_docs_landing_pages.py -q --no-cov -k reference -git checkout -- docs/src/reference/index.rst +cp "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst +diff "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst && echo restored ``` Expected: `test_the_index_and_the_toctree_are_one_ordered_list[reference]` FAILS. @@ -998,7 +1007,8 @@ Expected: `test_the_index_and_the_toctree_are_one_ordered_list[reference]` FAILS # (b) reach outside the section sed -i 's|^ :link: cli$| :link: ../howtos/units|' docs/src/reference/index.rst pixi run -e test pytest tests/test_docs_landing_pages.py -q --no-cov -k reference -git checkout -- docs/src/reference/index.rst +cp "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst +diff "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst && echo restored ``` Expected: three FAIL, all `[reference]`. `test_every_entry_links_to_a_page_in_its_own_section` fails with `reference's index links to ../howtos/units, outside the section` — the check this mutation is for. `test_the_index_and_the_toctree_are_one_ordered_list` fails because the card no longer matches the toctree's `cli`, and `test_the_index_lists_every_page_in_the_section` because `cli` is no longer listed. @@ -1014,7 +1024,8 @@ Expected: `test_the_index_lists_every_page_in_the_section[reference]` FAILS, nam # (d) the toctree shown sed -i '/^ :hidden:$/d' docs/src/reference/index.rst pixi run -e test pytest tests/test_docs_landing_pages.py -q --no-cov -k reference -git checkout -- docs/src/reference/index.rst +cp "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst +diff "${TMPDIR:-/tmp}/reference-index.rst" docs/src/reference/index.rst && echo restored ``` Expected: `test_the_toctree_is_hidden[reference]` FAILS. @@ -1026,7 +1037,7 @@ mv /tmp/api-light.svg docs/src/_static/cards/reference/api-light.svg ``` Expected: the fail-on-warning build exits non-zero, naming `api-light.svg`. **If it exits 0, do not add a gate:** record in the pull request that a missing icon builds clean, per `narrative spec §3.9` (presentation is ungated). -Finish with `git status --short` printing only this task's intended changes. +Finish with the final `diff` printing `restored` and `git status --short` printing only this task's intended changes. - [ ] **Step 6: Update the stylesheet's comments** From ee805748fe9fcbfd701e476c27b1614ad85d9af9 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Sep 2026 15:49:12 +0100 Subject: [PATCH 7/7] Add the changelog fragment for the reference cards Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C79QePZ862i61EJodupwVT --- changelog/330.documentation.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/330.documentation.rst diff --git a/changelog/330.documentation.rst b/changelog/330.documentation.rst new file mode 100644 index 0000000..2223a56 --- /dev/null +++ b/changelog/330.documentation.rst @@ -0,0 +1,7 @@ +The reference quadrant's landing page is now a grid of seven cards, each with an +icon drawn in the root page's vocabulary, in place of an introduction that listed +its pages by hand (narrative spec §3.9). ``tests/test_docs_landing_pages.py`` +holds the cards to the page's hidden toctree as it holds the other sections' +tables, and now refuses an entry that reaches outside its section with ``..``. +The root page's cards share the renamed ``teph-card`` classes, and its dark +*Tutorials* icon no longer rings its accent on the dark ground. (:user:`claude`)