From f8308e7627f1d4e4dfd356589e54f82af581bddd Mon Sep 17 00:00:00 2001 From: Gaurav Nelson <23069445+gaurav-nelson@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:51:27 +1000 Subject: [PATCH 1/2] feat: Add breadcrumb navigation, page actions toolbar, and copy page functionality --- layouts/_default/baseof.html | 1 + layouts/blog/list.html | 5 +- layouts/ci/single.html | 5 +- layouts/contribute/list.html | 5 +- layouts/contribute/single.html | 5 +- layouts/learn/list.html | 5 +- layouts/learn/single.html | 5 +- layouts/partials/breadcrumbs.html | 34 ++++ layouts/partials/page-actions.html | 47 +++++ layouts/partials/page-toolbar.html | 13 ++ layouts/partials/patterns-browser.html | 5 +- layouts/partials/patterns-index.html | 5 +- layouts/patterns/single.html | 5 +- static/css/custom.css | 234 +++++++++++++++++++++++++ static/js/codeblock.js | 6 +- static/js/page-actions.js | 150 ++++++++++++++++ 16 files changed, 492 insertions(+), 38 deletions(-) create mode 100644 layouts/partials/breadcrumbs.html create mode 100644 layouts/partials/page-actions.html create mode 100644 layouts/partials/page-toolbar.html create mode 100644 static/js/page-actions.js diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index c7bb0d272..76707add3 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -10,6 +10,7 @@ + - + {{ partial "page-toolbar.html" . }}
diff --git a/layouts/ci/single.html b/layouts/ci/single.html index e9b4c63af..36d6fb963 100644 --- a/layouts/ci/single.html +++ b/layouts/ci/single.html @@ -10,10 +10,7 @@
- - + {{ partial "page-toolbar.html" . }}

{{- .Title -}}

diff --git a/layouts/contribute/list.html b/layouts/contribute/list.html index 33c33428e..58a2e5009 100644 --- a/layouts/contribute/list.html +++ b/layouts/contribute/list.html @@ -7,10 +7,7 @@
- - + {{ partial "page-toolbar.html" . }}
diff --git a/layouts/contribute/single.html b/layouts/contribute/single.html index 1fdab2438..3dee51137 100644 --- a/layouts/contribute/single.html +++ b/layouts/contribute/single.html @@ -7,10 +7,7 @@
- - + {{ partial "page-toolbar.html" . }}
{{ .Content }} diff --git a/layouts/learn/list.html b/layouts/learn/list.html index c1d71754e..0c383aa22 100644 --- a/layouts/learn/list.html +++ b/layouts/learn/list.html @@ -7,10 +7,7 @@
- - + {{ partial "page-toolbar.html" . }}
diff --git a/layouts/learn/single.html b/layouts/learn/single.html index 3c7020d26..0dac53976 100644 --- a/layouts/learn/single.html +++ b/layouts/learn/single.html @@ -7,10 +7,7 @@
- - + {{ partial "page-toolbar.html" . }}
{{ .Content }} diff --git a/layouts/partials/breadcrumbs.html b/layouts/partials/breadcrumbs.html new file mode 100644 index 000000000..719ebeb01 --- /dev/null +++ b/layouts/partials/breadcrumbs.html @@ -0,0 +1,34 @@ +{{- if not .IsHome -}} +{{- $ancestors := .Ancestors.Reverse -}} +{{- $count := len $ancestors -}} + +{{- end -}} diff --git a/layouts/partials/page-actions.html b/layouts/partials/page-actions.html new file mode 100644 index 000000000..15b2adddd --- /dev/null +++ b/layouts/partials/page-actions.html @@ -0,0 +1,47 @@ +{{- $repo := .Site.Params.github_repo | default "validatedpatterns/docs" -}} +{{- $branch := .Site.Params.github_branch | default "main" -}} +{{- $filePath := "" -}} +{{- $rawUrl := "" -}} +{{- $sourceLabel := "View source" -}} +{{- if .File -}} + {{- $filePath = .File.Path -}} + {{- $rawUrl = printf "https://raw.githubusercontent.com/%s/%s/content/%s" $repo $branch $filePath -}} + {{- if strings.HasSuffix $filePath ".md" -}} + {{- $sourceLabel = "View as Markdown" -}} + {{- end -}} +{{- end -}} + +
+ + + +
diff --git a/layouts/partials/page-toolbar.html b/layouts/partials/page-toolbar.html new file mode 100644 index 000000000..62d696bc3 --- /dev/null +++ b/layouts/partials/page-toolbar.html @@ -0,0 +1,13 @@ +
+
+ {{ partial "sidebar-toggle.html" . }} +
+
+ {{ partial "breadcrumbs.html" . }} +
+ {{- if not (and .IsSection .Parent.IsHome) }} +
+ {{ partial "page-actions.html" . }} +
+ {{- end }} +
diff --git a/layouts/partials/patterns-browser.html b/layouts/partials/patterns-browser.html index 5bb3bda5b..f99240b26 100644 --- a/layouts/partials/patterns-browser.html +++ b/layouts/partials/patterns-browser.html @@ -10,10 +10,7 @@
- - + {{ partial "page-toolbar.html" . }}
diff --git a/layouts/partials/patterns-index.html b/layouts/partials/patterns-index.html index d27fa7d89..88998cb7c 100644 --- a/layouts/partials/patterns-index.html +++ b/layouts/partials/patterns-index.html @@ -5,10 +5,7 @@
- - + {{ partial "page-toolbar.html" . }}
diff --git a/layouts/patterns/single.html b/layouts/patterns/single.html index b4fd9fb13..7586def8f 100644 --- a/layouts/patterns/single.html +++ b/layouts/patterns/single.html @@ -7,10 +7,7 @@
- - + {{ partial "page-toolbar.html" . }}
{{ .Content }} diff --git a/static/css/custom.css b/static/css/custom.css index 77218c1b8..6c723bc2f 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1265,6 +1265,7 @@ h6 .anchor::before { display: block; } + /* Hide the header tools on mobile - they should only show when toggled */ .pf-c-page__header-tools { display: none; @@ -2651,3 +2652,236 @@ h6 .anchor::before { -moz-user-select: none; -ms-user-select: none; } + +/* ======================================================================== + Page Toolbar — sidebar toggle + breadcrumbs + page actions in one row + ======================================================================== */ + +.page-toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + margin-bottom: 1rem; +} + +.page-toolbar__toggle { + flex: 0 0 auto; + display: none; + position: static; + z-index: auto; +} + +.page-toolbar__breadcrumbs { + flex: 1 1 auto; + min-width: 0; +} + +.page-toolbar__actions { + flex: 0 0 auto; + margin-left: auto; +} + +.page-toolbar .sidebar-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0.375rem; + font-size: 0.625rem; + color: #151515; + background: transparent; + border: 1px solid #d2d2d2; + border-radius: 3px; + box-shadow: none; + transition: background-color 0.15s; +} + +.page-toolbar .sidebar-toggle:hover { + background: #f0f0f0; + box-shadow: none; + transform: none; +} + +.page-toolbar .sidebar-toggle:focus { + outline: 2px solid #06c; + outline-offset: 1px; +} + +/* ======================================================================== + Page Actions — split button with dropdown + ======================================================================== */ + +.page-actions { + position: relative; + display: inline-flex; + align-items: stretch; +} + +.page-actions__button { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + font-size: var(--pf-global--FontSize--sm, 0.875rem); + color: var(--pf-global--Color--100, #151515); + background: transparent; + border: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + border-radius: var(--pf-global--BorderRadius--sm, 3px) 0 0 var(--pf-global--BorderRadius--sm, 3px); + cursor: pointer; + transition: background-color 0.15s; + white-space: nowrap; + line-height: 1.5; +} + +.page-actions__button:hover { + background-color: var(--pf-global--BackgroundColor--200, #f0f0f0); +} + +.page-actions__button.is-copied { + color: var(--pf-global--success-color--100, #3e8635); +} + +.page-actions__toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + padding: 0.375rem; + font-size: 0.625rem; + color: var(--pf-global--Color--100, #151515); + background: transparent; + border: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + border-left: none; + border-radius: 0 var(--pf-global--BorderRadius--sm, 3px) var(--pf-global--BorderRadius--sm, 3px) 0; + cursor: pointer; + transition: background-color 0.15s; + line-height: 1; +} + +.page-actions__toggle:hover { + background-color: var(--pf-global--BackgroundColor--200, #f0f0f0); +} + +.page-actions__menu { + display: none; + position: absolute; + top: 100%; + right: 0; + z-index: 1000; + min-width: 200px; + margin: 0.25rem 0 0; + padding: 0.5rem 0; + list-style: none; + background-color: #fff; + border: 1px solid #d2d2d2; + border-radius: 3px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12), 0 0 2px rgba(0, 0, 0, 0.06); +} + +.page-actions__menu.is-open { + display: block; +} + +.page-actions__menu li { + list-style: none; +} + +.page-actions__menu button, +.page-actions__menu a { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 1rem; + font-size: var(--pf-global--FontSize--sm, 0.875rem); + color: var(--pf-global--Color--100, #151515); + background: none; + border: none; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + text-align: left; + line-height: 1.5; +} + +.page-actions__menu button:hover, +.page-actions__menu a:hover { + background-color: #f0f0f0; +} + +.page-actions__menu .page-actions__divider { + height: 1px; + margin: 0.375rem 0; + background-color: #d2d2d2; + padding: 0; +} + +.page-actions__menu i { + width: 1em; + text-align: center; + flex-shrink: 0; +} + +/* ======================================================================== + Breadcrumb truncation — collapse middle items on narrow screens + ======================================================================== */ + +.pf-c-breadcrumb__item--ellipsis { + display: none; +} + +.pf-c-breadcrumb__expand-btn { + background: none; + border: none; + cursor: pointer; + font-size: inherit; + line-height: inherit; + color: var(--pf-global--link--Color, #06c); + padding: 0; +} + +.pf-c-breadcrumb__expand-btn:hover { + color: var(--pf-global--link--Color--hover, #004080); + text-decoration: underline; +} + +.pf-c-breadcrumb.is-truncated .pf-c-breadcrumb__item--collapsible { + display: none; +} + +.pf-c-breadcrumb.is-truncated .pf-c-breadcrumb__item--ellipsis { + display: flex; +} + +/* Copy button label — full/short variants */ +.page-actions__label-short { + display: none; +} + +/* ======================================================================== + Responsive — show sidebar toggle inside toolbar on mobile/tablet + ======================================================================== */ + +@media (max-width: 1199px) { + .pf-c-page__sidebar:not(.active) ~ .pf-c-page__main .page-toolbar__toggle { + display: flex; + align-items: center; + } + + .page-actions__label-full { + display: none; + } + + .page-actions__label-short { + display: inline; + } +} + +@media (max-width: 576px) { + .page-actions__menu { + right: 0; + min-width: 180px; + } +} diff --git a/static/js/codeblock.js b/static/js/codeblock.js index c1c801e82..81d9439de 100644 --- a/static/js/codeblock.js +++ b/static/js/codeblock.js @@ -63,13 +63,15 @@ function codeWasCopied(button) { } function addCopyButtonToDom(button, highlightDiv) { - dataLang = highlightDiv.querySelector("pre > code[data-lang]").attributes["data-lang"].value; + const codeEl = highlightDiv.querySelector("pre > code[data-lang]"); + if (!codeEl) return; + dataLang = codeEl.attributes["data-lang"].value; highlightDiv.classList.add("pf-c-code-block__content") pre = highlightDiv.getElementsByTagName("pre"); pre[0].classList.add("pf-c-code-block__pre") code = pre[0].getElementsByTagName("code"); code[0].classList.add("pf-c-code-block__code") - console.log(highlightDiv); + //console.log(highlightDiv); highlightDiv.insertBefore(button, highlightDiv.firstChild); const codeBlock = createElementAndClass("div", "pf-c-code-block"); codeBlock.setAttribute("data-lang", dataLang) diff --git a/static/js/page-actions.js b/static/js/page-actions.js new file mode 100644 index 000000000..dba150d8c --- /dev/null +++ b/static/js/page-actions.js @@ -0,0 +1,150 @@ +(function () { + "use strict"; + + function initPageActions() { + document.querySelectorAll(".page-actions").forEach(function (container) { + var toggle = container.querySelector(".page-actions__toggle"); + var menu = container.querySelector(".page-actions__menu"); + var mainBtn = container.querySelector(".page-actions__button"); + var permalink = container.dataset.permalink || window.location.href; + + if (!toggle || !menu) return; + + toggle.addEventListener("click", function (e) { + e.stopPropagation(); + var expanded = toggle.getAttribute("aria-expanded") === "true"; + toggleMenu(!expanded); + }); + + mainBtn && + mainBtn.addEventListener("click", function () { + copyPageContent(mainBtn); + }); + + menu.querySelectorAll("[data-action]").forEach(function (btn) { + btn.addEventListener("click", function () { + var action = btn.dataset.action; + if (action === "copy-page") copyPageContent(btn); + else if (action === "copy-link") copyToClipboard(permalink, btn); + toggleMenu(false); + }); + }); + + function toggleMenu(show) { + menu.classList.toggle("is-open", show); + toggle.setAttribute("aria-expanded", String(show)); + } + + document.addEventListener("click", function (e) { + if (!container.contains(e.target)) toggleMenu(false); + }); + + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") toggleMenu(false); + }); + }); + } + + function initBreadcrumbTruncation() { + document.querySelectorAll(".pf-c-breadcrumb").forEach(function (nav) { + var list = nav.querySelector(".pf-c-breadcrumb__list"); + var expandBtn = nav.querySelector(".pf-c-breadcrumb__expand-btn"); + var manualExpand = false; + var fullWidth = 0; + + if (!list || !expandBtn) return; + + nav.classList.remove("is-truncated"); + + requestAnimationFrame(function () { + var items = list.querySelectorAll( + ".pf-c-breadcrumb__item:not(.pf-c-breadcrumb__item--ellipsis)" + ); + items.forEach(function (item) { + fullWidth += item.offsetWidth; + }); + + checkOverflow(); + + var resizeTimer; + window.addEventListener("resize", function () { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(checkOverflow, 80); + }); + }); + + function checkOverflow() { + if (manualExpand) return; + var wrapper = nav.closest(".page-toolbar__breadcrumbs"); + if (!wrapper) return; + nav.classList.toggle("is-truncated", fullWidth > wrapper.clientWidth); + } + + expandBtn.addEventListener("click", function () { + nav.classList.remove("is-truncated"); + manualExpand = true; + }); + }); + } + + function copyPageContent(triggerEl) { + var content = document.querySelector(".pf-c-content"); + if (!content) return; + + var text = content.innerText || content.textContent; + copyToClipboard(text, triggerEl); + } + + function copyToClipboard(text, triggerEl) { + if (!navigator.clipboard) { + fallbackCopy(text); + showFeedback(triggerEl); + return; + } + + navigator.clipboard.writeText(text).then(function () { + showFeedback(triggerEl); + }); + } + + function fallbackCopy(text) { + var ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { + document.execCommand("copy"); + } catch (_) { + /* noop */ + } + document.body.removeChild(ta); + } + + function showFeedback(el) { + if (!el) return; + + var label = el.querySelector(".page-actions__label") || el; + var original = label.innerHTML; + + label.innerHTML = "Copied!"; + el.classList.add("is-copied"); + + setTimeout(function () { + label.innerHTML = original; + el.classList.remove("is-copied"); + }, 1500); + } + + function init() { + initPageActions(); + initBreadcrumbTruncation(); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); From a7406e06bda26828db2e287dfa0291321d5889ab Mon Sep 17 00:00:00 2001 From: Gaurav Nelson <23069445+gaurav-nelson@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:08:20 +1000 Subject: [PATCH 2/2] Redesign TOC as a responsive component --- layouts/blog/single.html | 5 +- layouts/contribute/single.html | 5 +- layouts/learn/single.html | 5 +- layouts/partials/footer.html | 1 + layouts/partials/patterns-index.html | 5 +- layouts/partials/toc-mobile.html | 19 +++ layouts/partials/toc.html | 21 +-- layouts/patterns/single.html | 5 +- static/css/custom.css | 202 ++++++++++++++++++++------- static/js/toc.js | 99 +++++++++++++ 10 files changed, 298 insertions(+), 69 deletions(-) create mode 100644 layouts/partials/toc-mobile.html create mode 100644 static/js/toc.js diff --git a/layouts/blog/single.html b/layouts/blog/single.html index 428eee85b..19605bbf1 100644 --- a/layouts/blog/single.html +++ b/layouts/blog/single.html @@ -18,9 +18,11 @@
-
+
+ {{ partial "toc-mobile.html" . }}
+ {{ partial "toc.html" . }}

{{- .Title -}} @@ -45,7 +47,6 @@

{{ .Content }}

- {{ partial "toc.html" . }}
{{ partial "footer.html" . }} diff --git a/layouts/contribute/single.html b/layouts/contribute/single.html index 3dee51137..65eaa303e 100644 --- a/layouts/contribute/single.html +++ b/layouts/contribute/single.html @@ -5,14 +5,15 @@ {{ partial "menu-contribute.html" . }}
-
+
+ {{ partial "toc-mobile.html" . }} {{ partial "page-toolbar.html" . }}
+ {{ partial "toc.html" . }}
{{ .Content }}
- {{ partial "toc.html" . }}
{{ partial "footer.html" . }} diff --git a/layouts/learn/single.html b/layouts/learn/single.html index 0dac53976..0396a3878 100644 --- a/layouts/learn/single.html +++ b/layouts/learn/single.html @@ -5,10 +5,12 @@ {{ partial "menu-learn.html" . }}
-
+
+ {{ partial "toc-mobile.html" . }} {{ partial "page-toolbar.html" . }}
+ {{ partial "toc.html" . }}
{{ .Content }} @@ -39,7 +41,6 @@
- {{ partial "toc.html" . }}
{{ partial "footer.html" . }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html index 1e1ffbaa9..2bdb939d1 100644 --- a/layouts/partials/footer.html +++ b/layouts/partials/footer.html @@ -70,5 +70,6 @@ Red Hat logoCopyright © {{ now.Year }} Red Hat, Inc. All third-party trademarks are the property of their respective owners.
+ {{ partial "search-index.html" . }} diff --git a/layouts/partials/patterns-index.html b/layouts/partials/patterns-index.html index 88998cb7c..a4f247710 100644 --- a/layouts/partials/patterns-index.html +++ b/layouts/partials/patterns-index.html @@ -3,10 +3,12 @@ {{ partial "menu-patterns.html" . }}
-
+
+ {{ partial "toc-mobile.html" . }} {{ partial "page-toolbar.html" . }}
+ {{ partial "toc.html" . }}
@@ -106,7 +108,6 @@

{{ .Title }}

- {{ partial "toc.html" . }}
{{ partial "footer.html" . }} diff --git a/layouts/partials/toc-mobile.html b/layouts/partials/toc-mobile.html new file mode 100644 index 000000000..71e1aef23 --- /dev/null +++ b/layouts/partials/toc-mobile.html @@ -0,0 +1,19 @@ +{{- $toc := .TableOfContents -}} +{{- if and $toc (not (eq $toc "")) -}} + +{{- $tocHTML := $toc | replaceRE " id=\"TableOfContents\"" "" | replaceRE "
    " "
      " | replaceRE "
    • " "
    • " | replaceRE " + +
      + {{- $tocHTML | safeHTML }} +
      + + +{{- end -}} diff --git a/layouts/partials/toc.html b/layouts/partials/toc.html index 0c3582f0d..fc3b0a4fe 100644 --- a/layouts/partials/toc.html +++ b/layouts/partials/toc.html @@ -1,10 +1,13 @@ - + +{{- end -}} diff --git a/layouts/patterns/single.html b/layouts/patterns/single.html index 7586def8f..387db3d1e 100644 --- a/layouts/patterns/single.html +++ b/layouts/patterns/single.html @@ -5,10 +5,12 @@ {{ partial "menu-patterns.html" . }}
-
+
+ {{ partial "toc-mobile.html" . }} {{ partial "page-toolbar.html" . }}
+ {{ partial "toc.html" . }}
{{ .Content }} @@ -39,7 +41,6 @@
- {{ partial "toc.html" . }}
{{ partial "footer.html" . }} diff --git a/static/css/custom.css b/static/css/custom.css index 6c723bc2f..d1820b397 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -211,13 +211,6 @@ td > code { border-color: #f3f3f3; } -.pf-c-jump-links__link.pf-m-current, -.pf-c-jump-links__link.pf-m-current:hover, -.pf-c-jump-links__link.pf-m-current:not(.pf-m-expanded) -.pf-c-nav__link { - color: var(--pf-c-nav__link--m-current--Color); - background-color: var(--pf-c-nav__link--m-current--BackgroundColor); -} /* Makes nav items like accordion items */ /* Need this to unify the look of the pages */ @@ -439,39 +432,172 @@ main.pf-c-page__main { } } -/* STICKY NAV ON THE LEFT */ - +/* Sticky positioning used by CI key sidebar */ .sticky { + position: sticky; + top: 0; + align-self: flex-start; + padding: 40px var(--pf-global--spacer--lg); +} + +/* ON THIS PAGE - Desktop sidebar */ + +.otp-desktop { max-height: calc(100vh - 76px); overflow-y: auto; - /* Hide TOC scrollbar IE, Edge & Firefox */ - /*-ms-overflow-style: none; - scrollbar-width: none; */ order: 1; - padding: 40px var(--pf-global--spacer--lg) var(--pf-global--spacer--lg) - var(--pf-global--spacer--2xl); - /* flex-grow: 1; */ + padding: 40px var(--pf-global--spacer--lg) var(--pf-global--spacer--lg) var(--pf-global--spacer--xl); background: none; margin: 0; position: sticky; top: 0; - min-width: 250px; /* Reduced from 400px */ + max-width: 240px; + flex-shrink: 0; align-self: flex-start; z-index: 501; } -/* Mobile adjustments for sticky navigation */ -@media (max-width: 992px) { - .sticky { - position: relative; - min-width: 100%; - max-height: none; - padding: var(--pf-global--spacer--md); - } +.otp-desktop__header { + margin-bottom: 12px; +} + +.otp-desktop__title { + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pf-global--Color--200, #6a6e73); + margin: 0; } -.active { - background-color: red; +/* ON THIS PAGE - Mobile collapsible bar */ + +.otp-mobile { + display: none; +} + +/* Shared link/list styles */ + +.otp-list { + list-style: none; + padding: 0; + margin: 0; +} + +.otp-list .otp-list { + padding-left: 16px; +} + +.otp-item { + margin: 0; +} + +.otp-link { + display: block; + padding: 4px 0; + font-size: 0.85rem; + line-height: 1.4; + color: var(--pf-global--Color--200, #6a6e73); + text-decoration: none; + border-left: 2px solid transparent; + padding-left: 12px; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.otp-link:hover { + color: var(--pf-global--link--Color, #06c); + text-decoration: none; +} + +.otp-link.active { + color: var(--pf-global--link--Color, #06c); + border-left-color: var(--pf-global--link--Color, #06c); + font-weight: 500; +} + +.otp-list .otp-list .otp-link { + font-size: 0.8rem; +} + +@media (max-width: 1024px) { + .otp-desktop { + display: none !important; + } + + .otp-mobile { + display: block; + width: 100%; + margin-bottom: 0; + position: sticky; + top: 0; + z-index: 500; + background-color: var(--pf-global--BackgroundColor--light-100, #fff); + border-bottom: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + } + + .otp-mobile__toggle { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 15px; + background: none; + border: none; + cursor: pointer; + font-size: 0.85rem; + color: var(--pf-global--Color--200, #6a6e73); + text-align: left; + white-space: nowrap; + overflow: hidden; + } + + .otp-mobile__toggle:hover { + color: var(--pf-global--Color--100, #151515); + } + + .otp-mobile__label { + font-weight: 600; + flex-shrink: 0; + } + + .otp-mobile__chevron { + font-size: 0.65rem; + flex-shrink: 0; + transition: transform 0.2s ease; + } + + .otp-mobile.is-open .otp-mobile__chevron { + transform: rotate(90deg); + } + + .otp-mobile__current { + color: var(--pf-global--Color--100, #151515); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + + .otp-mobile__dropdown { + display: none; + padding: 0 15px 12px 15px; + background-color: var(--pf-global--BackgroundColor--light-100, #fff); + } + + .otp-mobile.is-open .otp-mobile__dropdown { + display: block; + } + + .otp-mobile .otp-link { + padding: 6px 12px; + border-left: none; + } + + .otp-mobile .otp-link:hover, + .otp-mobile .otp-link.active { + background-color: var(--pf-global--BackgroundColor--200, #f0f0f0); + border-radius: 4px; + } } /* SCROLLSPY */ @@ -490,24 +616,6 @@ nav.ul.li.a { font-size: 0.8rem; } -/* Emphasizes that second levels in the toc are further inside*/ -aside ul ul { - margin-left: 20px !important; -} -/* Makes the font size smaller inside the TOC */ -nav ul ul ul a { - font-size: 0.8rem !important; -} - -.pf-c-jump-links__link { - padding-top: 0; - font-size: 0.9rem; - color: var(--pf-global--icon--Color--light); -} - -.pf-c-jump-links__link:hover { - color: var(--pf-global--link--Color--light); -} a code { display: contents; @@ -1394,12 +1502,6 @@ h6 .anchor::before { display: block !important; } - /* Hide TOC on tablets and mobile */ - .pf-c-jump-links, - aside.pf-c-jump-links { - display: none !important; - } - /* Main content takes full width */ .pf-c-content { width: 100% !important; diff --git a/static/js/toc.js b/static/js/toc.js new file mode 100644 index 000000000..6482f08ee --- /dev/null +++ b/static/js/toc.js @@ -0,0 +1,99 @@ +document.addEventListener('DOMContentLoaded', function () { + var mobileNav = document.querySelector('.otp-mobile'); + var desktopNav = document.querySelector('.otp-desktop'); + + if (!mobileNav && !desktopNav) return; + + var toggle = mobileNav ? mobileNav.querySelector('.otp-mobile__toggle') : null; + var currentLabel = mobileNav ? mobileNav.querySelector('.otp-mobile__current') : null; + + // Mobile toggle + if (toggle) { + toggle.addEventListener('click', function () { + var expanded = mobileNav.classList.toggle('is-open'); + toggle.setAttribute('aria-expanded', expanded); + }); + } + + // Close mobile dropdown when a link is clicked + if (mobileNav) { + mobileNav.addEventListener('click', function (e) { + if (e.target.closest('.otp-link')) { + mobileNav.classList.remove('is-open'); + if (toggle) toggle.setAttribute('aria-expanded', 'false'); + } + }); + } + + // Close mobile dropdown on outside click + document.addEventListener('click', function (e) { + if (mobileNav && !mobileNav.contains(e.target)) { + mobileNav.classList.remove('is-open'); + if (toggle) toggle.setAttribute('aria-expanded', 'false'); + } + }); + + // Close mobile dropdown on resize to desktop + window.addEventListener('resize', function () { + if (window.innerWidth > 1024 && mobileNav) { + mobileNav.classList.remove('is-open'); + if (toggle) toggle.setAttribute('aria-expanded', 'false'); + } + }); + + // ScrollSpy active-state mirroring and current-section label + setupScrollSpy(); + + function setupScrollSpy() { + var headingIds = []; + var allLinks = document.querySelectorAll('.otp-link'); + + allLinks.forEach(function (link) { + var href = link.getAttribute('href'); + if (href && href.startsWith('#')) { + headingIds.push(href.substring(1)); + } + }); + + if (headingIds.length === 0) return; + + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + setActive(entry.target.id); + } + }); + }, { + rootMargin: '-80px 0px -60% 0px', + threshold: 0 + }); + + headingIds.forEach(function (id) { + var el = document.getElementById(id); + if (el) observer.observe(el); + }); + + if (headingIds.length > 0) { + setActive(headingIds[0]); + } + } + + function setActive(id) { + var allLinks = document.querySelectorAll('.otp-link'); + allLinks.forEach(function (link) { + var href = link.getAttribute('href'); + if (href === '#' + id) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + + if (currentLabel) { + var activeLink = document.querySelector('.otp-mobile .otp-link.active'); + if (activeLink) { + currentLabel.textContent = activeLink.textContent; + } + } + } +});