From f19cf1a4cd8f984184a30f56dd44356b8335a955 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 08:38:12 -0700 Subject: [PATCH 1/6] SEO improvements and indexing --- docs/google41ec26928c99670f.html | 1 + docs/libraries/beautifulsoup.md | 2 +- docs/libraries/collections.md | 5 ++-- docs/libraries/index.md | 1 + docs/libraries/turtle.md | 3 +-- mkdocs.yml | 3 +++ overrides/main.html | 42 ++++++++++++++++++++++++++++++++ 7 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 docs/google41ec26928c99670f.html create mode 100644 overrides/main.html diff --git a/docs/google41ec26928c99670f.html b/docs/google41ec26928c99670f.html new file mode 100644 index 0000000..0582812 --- /dev/null +++ b/docs/google41ec26928c99670f.html @@ -0,0 +1 @@ +google-site-verification: google41ec26928c99670f.html \ No newline at end of file diff --git a/docs/libraries/beautifulsoup.md b/docs/libraries/beautifulsoup.md index bbc7e6c..c19baa5 100644 --- a/docs/libraries/beautifulsoup.md +++ b/docs/libraries/beautifulsoup.md @@ -1,7 +1,7 @@ --- description: >- Parsing HTML in Python with BeautifulSoup: finding tags, reading attributes and text, and - turning a page into structured data — the piece that pairs with requests to scrape a page. + turning a page into structured data for web scraping. --- # :material-pot-steam-outline:{ .lg .middle } BeautifulSoup library diff --git a/docs/libraries/collections.md b/docs/libraries/collections.md index 0f56df1..51862aa 100644 --- a/docs/libraries/collections.md +++ b/docs/libraries/collections.md @@ -1,8 +1,7 @@ --- description: >- - Specialized container types beyond list/dict/tuple/set in Python's collections module: - Counter, defaultdict, namedtuple, deque, OrderedDict, ChainMap, and the User* wrapper - classes, with runnable examples. + Specialized container types in Python's collections module: Counter, defaultdict, + namedtuple, deque, OrderedDict, and ChainMap, with runnable examples. --- # :material-format-list-group:{ .lg .middle } collections library diff --git a/docs/libraries/index.md b/docs/libraries/index.md index f88a0a5..1883887 100644 --- a/docs/libraries/index.md +++ b/docs/libraries/index.md @@ -1,4 +1,5 @@ --- +title: Libraries description: An overview of popular Python libraries covered on this site, both built-in and third-party. hide: - navigation diff --git a/docs/libraries/turtle.md b/docs/libraries/turtle.md index 0004a79..fa6b504 100644 --- a/docs/libraries/turtle.md +++ b/docs/libraries/turtle.md @@ -1,8 +1,7 @@ --- description: >- Building small movement-based games in Python with the turtle module: window setup, - positions and motion, drawing shapes, the animation loop, keyboard and mouse input, and - collision detection. + motion, drawing shapes, the animation loop, and collision detection. --- # :material-turtle:{ .lg .middle } Turtle library diff --git a/mkdocs.yml b/mkdocs.yml index d639c1b..2c364c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,8 @@ site_name: Python Field Guide site_url: https://pythonfieldguide.com +site_description: >- + A free Python reference with runnable code examples covering data types, collections, + loops, functions, classes, error handling, and popular libraries. hooks: - hooks/thanks_url.py diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..0d4914f --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block extrahead %} + {% set og_description = page.meta.description if page and page.meta and page.meta.description else config.site_description %} + {% set page_title = page.meta.title if page and page.meta and page.meta.title else page.title %} + {% set og_title = page_title ~ " - " ~ config.site_name if page_title and not page.is_homepage else config.site_name %} + {% set og_image = config.site_url ~ "img/favicon-large.png" %} + + + + + + + + + + + {% set jsonld_name = page_title if page_title and not page.is_homepage else config.site_name %} + {% set page_url = page.canonical_url | default(config.site_url, true) %} + {% set jsonld = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + "@id": config.site_url ~ "#website", + "url": config.site_url, + "name": config.site_name, + "description": config.site_description + }, + { + "@type": "WebPage", + "@id": page_url, + "url": page_url, + "name": jsonld_name, + "description": og_description | default(config.site_description, true), + "isPartOf": { "@id": config.site_url ~ "#website" }, + "inLanguage": config.theme.language | default("en", true) + } + ] + } %} + +{% endblock %} From 0f9d3cce6afd714179f70cb87540d2c035d1656d Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 21:43:44 -0700 Subject: [PATCH 2/6] mobile toggle and menu styling --- docs/javascripts/essentials_toggle.js | 10 +++- docs/stylesheets/extra.css | 73 ++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/docs/javascripts/essentials_toggle.js b/docs/javascripts/essentials_toggle.js index b672de8..91ac99a 100644 --- a/docs/javascripts/essentials_toggle.js +++ b/docs/javascripts/essentials_toggle.js @@ -96,15 +96,21 @@ essentials.type = "button"; essentials.className = "pt-simplify-option"; essentials.dataset.mode = "simplified"; - essentials.textContent = "Essentials"; essentials.title = "Show only what you need to write your first programs"; + const essentialsLabel = document.createElement("span"); + essentialsLabel.className = "pt-simplify-label"; + essentialsLabel.textContent = "Essentials"; + essentials.append(essentialsLabel); const advanced = document.createElement("button"); advanced.type = "button"; advanced.className = "pt-simplify-option"; advanced.dataset.mode = "advanced"; - advanced.textContent = "Advanced"; advanced.title = "Show all site content"; + const advancedLabel = document.createElement("span"); + advancedLabel.className = "pt-simplify-label"; + advancedLabel.textContent = "Advanced"; + advanced.append(advancedLabel); container.append(highlight, advanced, essentials); paletteForm.insertAdjacentElement("beforebegin", container); diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index aa81a32..25831d5 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -809,13 +809,74 @@ input:checked + .md-consent__settings { color: var(--pt-bg); } +/* Below ~45em (matches this file's other mobile breakpoints) the + "Essentials"/"Advanced" words get tight — swap each option's text for an + icon, same mask-image technique as .pt-theme-option's sun/moon. Icon + paths are Google's Material Symbols "psychiatry" and "park" (tree) + glyphs (viewBox 0 -960 960 960, their native coordinate space). The + label text stays in the DOM (not aria-hidden) so the button's accessible + name is unchanged for screen readers; only the visual text is clipped. */ +.pt-simplify-option[data-mode]::before { + content: ""; + display: none; + width: 0.7rem; + height: 0.7rem; + background-color: currentColor; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-position: center; + mask-position: center; +} + +.pt-simplify-option[data-mode="simplified"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E"); +} + +.pt-simplify-option[data-mode="advanced"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E"); +} + +@media (max-width: 45em) { + .pt-simplify-option[data-mode]::before { + display: block; + } + + .pt-simplify-toggle:has(.pt-simplify-option[data-mode]) .pt-simplify-option { + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.4rem; + } + + .pt-simplify-option[data-mode] .pt-simplify-label { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + } +} + /* Several of Material's own rules (.md-header__option, .md-typeset details, .md-option:checked + label) beat the plain [hidden] { display: none } UA rule on specificity, so an element we hide via JS (.hidden = true) can silently stay visible — hit this three times now (the palette form, admonitions under a hidden heading, the old knob). One global - override instead of chasing each element type. */ -[hidden] { + override instead of chasing each element type. + Excludes Material's own [data-md-component="sidebar"] (the mobile nav + drawer, reused as .md-sidebar--primary): Material toggles its `hidden` + attribute itself and reveals it via its own more-specific (but + non-!important) CSS when the drawer checkbox is checked — our + !important here silently beat that and left the hamburger menu + permanently empty on mobile, with no console error. Same class of bug + as the .md-sidebar--primary gotcha in CLAUDE.md's Navigation section, + different cause. */ +[hidden]:not([data-md-component="sidebar"]) { display: none !important; } @@ -842,13 +903,13 @@ input:checked + .md-consent__settings { } .pt-theme-option[data-scheme="default"]::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M579-381q41-41 41-99t-41-99q-41-41-99-41t-99 41q-41 41-41 99t41 99q41 41 99 41t99-41Zm-240.5 42.5Q280-397 280-480t58.5-141.5Q397-680 480-680t141.5 58.5Q680-563 680-480t-58.5 141.5Q563-280 480-280t-141.5-58.5ZM200-450H40v-60h160v60Zm720 0H760v-60h160v60ZM450-760v-160h60v160h-60Zm0 720v-160h60v160h-60ZM262-658l-100-97 43-44 96 100-39 41Zm494 496-98-100 41-41 99 98-42 43Zm-99-537 98-99 44 42-99 98-43-41ZM162-205l99-98 42 42-98 99-43-43Zm318-275Z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M579-381q41-41 41-99t-41-99q-41-41-99-41t-99 41q-41 41-41 99t41 99q41 41 99 41t99-41Zm-240.5 42.5Q280-397 280-480t58.5-141.5Q397-680 480-680t141.5 58.5Q680-563 680-480t-58.5 141.5Q563-280 480-280t-141.5-58.5ZM200-450H40v-60h160v60Zm720 0H760v-60h160v60ZM450-760v-160h60v160h-60Zm0 720v-160h60v160h-60ZM262-658l-100-97 43-44 96 100-39 41Zm494 496-98-100 41-41 99 98-42 43Zm-99-537 98-99 44 42-99 98-43-41ZM162-205l99-98 42 42-98 99-43-43Zm318-275Z'/%3E%3C/svg%3E"); } .pt-theme-option[data-scheme="slate"]::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480-120Zm0-60q109 0 190-67.5T771-406q-25 11-53.67 16.5Q688.67-384 660-384q-114.69 0-195.34-80.66Q384-545.31 384-660q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180-480q0 125 87.5 212.5T480-180Zm-4-297Z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480-120Zm0-60q109 0 190-67.5T771-406q-25 11-53.67 16.5Q688.67-384 660-384q-114.69 0-195.34-80.66Q384-545.31 384-660q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180-480q0 125 87.5 212.5T480-180Zm-4-297Z'/%3E%3C/svg%3E"); } .simplify-active [data-advanced] { From 08d067ae1d47e2926a3cebde684195ce5edf18b7 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 22:06:23 -0700 Subject: [PATCH 3/6] mobile nav spacing --- docs/stylesheets/extra.css | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 25831d5..5e1fab6 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -164,6 +164,40 @@ html:focus-within::-webkit-scrollbar-thumb { font-weight: 800; } +/* Below ~45em the header's own title font-size (Material's default) plus + the Essentials/Advanced + light/dark icon toggles leave too little room + for the full "Python Field Guide" site name — Material ellipsis- + truncates it to "Python Field...". Shrink the text instead of cutting + the name down to one word, so the full brand name stays intact. */ +@media (max-width: 45em) { + .md-header__title .md-header__topic { + font-size: 0.7rem; + } + + /* The title text and the Essentials/Advanced + light/dark toggles are + both vertically centered in the same header row, but the toggles' + box-bottom sits 4px below the text's glyph-bottom (line-height + centering isn't the same as optical/visual centering) — nudge the + text down so the two lines up. */ + .md-header__title .md-header__ellipsis { + position: relative; + top: 4px; + } + + /* Material's own header title margin (20px left / 8px right, from its + `[dir="ltr"] .md-header__title` rule — matched here to beat that + selector's specificity) doesn't match the 4px margins the + hamburger/toggles/search icon all already share, so the five header + items — hamburger, title, Essentials/Advanced toggle, light/dark + toggle, search — read with uneven gaps between them (24 / 12 / 8 / + 8px). Match the title's margins to the other items' 4px so every gap + comes out to the same 8px. */ + [dir="ltr"] .md-header__title { + margin-left: 0.2rem; + margin-right: 0.2rem; + } +} + /* Decorative snake-scale header, slate (dark) scheme only. Layered on top of .md-header's own background-color (the translucent --md-primary-fg-color set above), so the blur/hairline border above still apply underneath. From d5129c648e4dbc9c624a65c51eff714d82b5a4ca Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 22:33:43 -0700 Subject: [PATCH 4/6] h1 icon height same as h1 text --- docs/stylesheets/extra.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 5e1fab6..4a242e5 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -5,6 +5,19 @@ font-family: "Cormorant Garamond", serif; } +/* Every h1 on the site leads with an attr_list icon (e.g. `:material-cube- + outline:{ .lg .middle }`). Material's own `.lg` class sizes it to a + fixed 48px regardless of the heading's own font-size, and `.middle` + vertical-centers it — both wrong once the heading text isn't also 48px + tall (it never is: h1 is 32px by default, 24px on mobile below). Size + the icon to `1em` so it tracks the heading's own font-size at any + breakpoint, and bottom-align it to the text instead of centering it. */ +.md-typeset h1 .twemoji { + width: 1em; + height: 1em; + vertical-align: text-bottom; +} + :root, [data-md-color-scheme="default"] { --pt-bg: #F0ECE4; From a76bd04b4f169b92158f469388dfe2879e66a986 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 22:46:35 -0700 Subject: [PATCH 5/6] mobile responsiveness styling and spacing --- docs/stylesheets/extra.css | 83 +++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 4a242e5..31a7e0b 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1398,12 +1398,93 @@ input:checked + .md-consent__settings { color: var(--pt-heading-h2); } +/* Material's default h1 (2em = 32px) and this site's own h2 override just + above (2.375em = 38px) both read oversized on a narrow phone viewport + relative to the body text around them — shrink both below the site's + usual ~45em mobile breakpoint. Placed after the h2 rule above (same + specificity, `.md-typeset h2`) so it wins by source order regardless of + viewport — a copy of this placed earlier in the file loses that fight + even inside a matching media query. */ +@media (max-width: 45em) { + .md-typeset h1 { + font-size: 1.5em; + /* Not equal CSS values: h1's line-height already carries ~8px of + intrinsic leading above the glyph that margin-top doesn't need to + add to, but there's nothing similar below — so margin-top is + shorted by that amount to make the two *visually* equal (both + ~0.8rem, matching the page's side gutter, measured edge-to-edge in + the browser rather than by their CSS values). */ + margin-top: 0.4rem; + margin-bottom: 0.8rem; + } + + .md-typeset h2 { + font-size: 1.75em; + } + + /* The visible gap below h1 actually comes from the *next* element's own + top margin — most often .pfg-section's `margin: 4em 0` — not from h1 + itself. Zero that out so the space below h1 is controlled by h1's own + margin-bottom alone (adjacent margins collapse to the larger one, so + this doesn't add on top of it). `.md-typeset .pfg-section` is two + classes (0,2,0); the generic `* ` fallback below is only (0,1,1) and + loses to it, so .pfg-section needs its own higher-specificity + override alongside. */ + .md-typeset h1 + * { + margin-top: 0; + } + + .md-typeset h1 + .pfg-section { + margin-top: 0; + } + + /* The gap *above* h1 wasn't from h1 at all — it's Material's own + `.md-main__inner { margin-top: 1.5rem }` and `.md-content__inner + { padding-top: 0.6rem }`, i.e. the page's fixed top offset under the + header, unrelated to h1's own (already-zero) margin-top. Remove that + built-in offset so h1's own 0.8rem margin-top above is the only thing + producing the gap, matching the 0.8rem (same as the .pfg-section/page + side gutter) margin-bottom below. */ + .md-main__inner { + margin-top: 0; + } + + .md-content__inner { + padding-top: 0; + } + + /* Material's own `.md-copyright { width: 100% }` deliberately stacks the + footer's copyright line above the social icon row below its 45em + breakpoint (only overridden to `width: auto` at 45em+, letting them + share a row) — there's plenty of horizontal room for both on a phone + regardless, so force the same side-by-side layout down here too. */ + .md-copyright { + width: auto; + } +} + .md-typeset .pfg-section { background: var(--pt-panel); border: 1px solid var(--pt-hairline); border-radius: 8px; padding: 0.5em 2em 2em; - margin: 4em 0; + /* No margin-bottom: whatever follows already supplies its own leading + space (another .pfg-section's margin-top, an h2's own top margin, + or the page footer), so a margin here would just double it up. */ + margin: 4em 0 0; +} + +/* Halved from the padding above (0.5em 2em 2em) — sized for desktop's + wider content column, it eats too much of a narrow phone viewport. + Placed after the base rule above (same specificity, `.md-typeset + .pfg-section`) so it wins by source order regardless of viewport. */ +@media (max-width: 45em) { + .md-typeset .pfg-section { + padding: 0.25em 1em 1em; + /* Halved from the margin-top above (4em) for the same reason as the + padding above it. */ + margin-top: 2em; + } } .md-typeset .pfg-section > h2:first-child, From bc6237bc222904850f4790a3c13cfb12280e2578 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sun, 20 Sep 2026 23:09:16 -0700 Subject: [PATCH 6/6] toast for advanced toggle --- docs/javascripts/essentials_toggle.js | 60 ++++++++++++++++ docs/stylesheets/extra.css | 100 +++++++++++++++++++++----- 2 files changed, 144 insertions(+), 16 deletions(-) diff --git a/docs/javascripts/essentials_toggle.js b/docs/javascripts/essentials_toggle.js index 91ac99a..5f9a318 100644 --- a/docs/javascripts/essentials_toggle.js +++ b/docs/javascripts/essentials_toggle.js @@ -63,6 +63,63 @@ }); } + // Disappearing confirmation toast for the Essentials/Advanced toggle — + // the toggle itself only shows the current state, not what just changed, + // so a click gives no feedback about its actual effect otherwise. + let toastTimer = null; + function showToast(active) { + let toast = document.getElementById("pt-toast"); + if (!toast) { + toast = document.createElement("div"); + toast.id = "pt-toast"; + toast.className = "pt-toast"; + // status + polite: announced to screen readers without interrupting + // whatever they're already reading, same as a visual toast doesn't + // steal focus. + toast.setAttribute("role", "status"); + toast.setAttribute("aria-live", "polite"); + document.body.appendChild(toast); + } + + toast.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "pt-toast__title"; + const icon = document.createElement("span"); + icon.className = "pt-mode-icon"; + icon.dataset.mode = active ? "simplified" : "advanced"; + icon.setAttribute("aria-hidden", "true"); + title.append(active ? "Essentials " : "Advanced ", icon); + + const body = document.createElement("div"); + body.className = "pt-toast__body"; + body.textContent = active + ? "Just the basics, start here!" + : "Viewing all content."; + + toast.append(title, body); + + // The header's own height isn't fixed across breakpoints (taller with + // the tab bar on tablet/desktop) or over time (Material can hide/reveal + // it on scroll), so position below it fresh on every call rather than + // hardcoding an offset in CSS. + const header = document.querySelector(".md-header"); + const headerBottom = header ? header.getBoundingClientRect().bottom : 0; + toast.style.top = Math.max(headerBottom, 0) + 12 + "px"; + + // Retrigger the transition even if a toast is already showing (rapid + // clicks between the two options): drop the class, force layout, then + // re-add it, instead of just extending the existing timer. + toast.classList.remove("pt-toast--visible"); + void toast.offsetWidth; + toast.classList.add("pt-toast--visible"); + + clearTimeout(toastTimer); + toastTimer = setTimeout(function () { + toast.classList.remove("pt-toast--visible"); + }, 1400); + } + function applyState(container, active) { document.body.classList.toggle("simplify-active", active); updateLibrarySpans(); @@ -119,8 +176,11 @@ const option = event.target.closest(".pt-simplify-option"); if (!option) return; const next = option.dataset.mode === "simplified"; + const wasActive = container.dataset.active === "simplified"; + if (next === wasActive) return; localStorage.setItem(STORAGE_KEY, String(next)); applyState(container, next); + showToast(next); }); return container; diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 31a7e0b..ea9c98a 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -834,6 +834,9 @@ input:checked + .md-consent__settings { .pt-simplify-option { position: relative; z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; flex: 1; padding: 0 0.5rem; border: none; @@ -856,18 +859,26 @@ input:checked + .md-consent__settings { color: var(--pt-bg); } -/* Below ~45em (matches this file's other mobile breakpoints) the - "Essentials"/"Advanced" words get tight — swap each option's text for an - icon, same mask-image technique as .pt-theme-option's sun/moon. Icon - paths are Google's Material Symbols "psychiatry" and "park" (tree) - glyphs (viewBox 0 -960 960 960, their native coordinate space). The - label text stays in the DOM (not aria-hidden) so the button's accessible - name is unchanged for screen readers; only the visual text is clipped. */ -.pt-simplify-option[data-mode]::before { +/* Icon for each option (Material Symbols "psychiatry" / "park", viewBox + 0 -960 960 960), same mask-image technique as .pt-theme-option's + sun/moon. On desktop it trails the "Essentials"/"Advanced" word — an + ::after, not ::before, so it renders after the label text in normal + document order without any extra markup or flex re-ordering; the + option button itself is a flex row (see .pt-simplify-option above), so + the icon centers vertically against the text the same way .pt-theme- + option's icon-only button centers its own icon — no vertical-align + fudging needed, and it can't drift out of sync between the two variants. + Below ~45em (matches this file's other mobile breakpoints) the words + get tight, so the label is visually clipped and the icon alone + represents the option — it stays in the DOM (not aria-hidden) so the + button's accessible name is unchanged for screen readers either way. */ +.pt-simplify-option[data-mode]::after, +.pt-mode-icon { content: ""; - display: none; + display: inline-block; width: 0.7rem; height: 0.7rem; + flex: none; background-color: currentColor; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; @@ -877,25 +888,28 @@ input:checked + .md-consent__settings { mask-position: center; } -.pt-simplify-option[data-mode="simplified"]::before { +.pt-simplify-option[data-mode]::after { + margin-left: 0.3rem; +} + +.pt-simplify-option[data-mode="simplified"]::after, +.pt-mode-icon[data-mode="simplified"] { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E"); } -.pt-simplify-option[data-mode="advanced"]::before { +.pt-simplify-option[data-mode="advanced"]::after, +.pt-mode-icon[data-mode="advanced"] { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E"); } @media (max-width: 45em) { - .pt-simplify-option[data-mode]::before { - display: block; + .pt-simplify-option[data-mode]::after { + margin-left: 0; } .pt-simplify-toggle:has(.pt-simplify-option[data-mode]) .pt-simplify-option { - display: flex; - align-items: center; - justify-content: center; padding: 0 0.4rem; } @@ -959,6 +973,60 @@ input:checked + .md-consent__settings { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480-120Zm0-60q109 0 190-67.5T771-406q-25 11-53.67 16.5Q688.67-384 660-384q-114.69 0-195.34-80.66Q384-545.31 384-660q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180-480q0 125 87.5 212.5T480-180Zm-4-297Z'/%3E%3C/svg%3E"); } +/* Disappearing confirmation toast for the Essentials/Advanced toggle + (docs/javascripts/essentials_toggle.js's showToast). Sits just below the + header — right where the toggle that triggered it lives — rather than + at the bottom of the screen, so it's immediately next to the control the + reader just clicked. `top` itself is set inline by showToast() (the + header's own height varies: ~48px on mobile, taller on desktop/tablet + with the tab bar, and Material can also hide/reveal it on scroll), only + the offset-from-header gap lives here. Centered horizontally on both + desktop and mobile. Colors are inverted (ink background, bg-colored + text) same as the toggle's own active-option highlight, so it reads as + "this site's accent chip," not a generic OS notification. */ +.pt-toast { + position: fixed; + z-index: 1000; + left: 50%; + transform: translate(-50%, 0); + max-width: min(90vw, 22rem); + padding: 0.6rem 1rem; + border-radius: 0.4rem; + background-color: var(--pt-ink); + color: var(--pt-bg); + font-size: 0.75rem; + line-height: 1.4; + text-align: center; + box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.25); + opacity: 0; + pointer-events: none; + /* Fade only — no slide/transform. The fade-out itself is slow and + gentle; showToast() in essentials_toggle.js keeps the fully-visible + hold time short instead, so the toast still clears the screen quickly + overall. */ + transition: opacity 0.7s ease; +} + +.pt-toast--visible { + opacity: 1; + /* Fading in can be quick; only the fade-out (the base .pt-toast rule + above, used once this class is removed again) is slow. */ + transition: opacity 0.15s ease; +} + +.pt-toast__title { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + font-weight: 700; + margin-bottom: 0.2rem; +} + +.pt-toast__body { + font-weight: 400; +} + .simplify-active [data-advanced] { display: none; }