mkdocs-nested-tabs 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,45 @@
1
+ import os
2
+
3
+ from mkdocs.config import config_options
4
+ from mkdocs.plugins import BasePlugin
5
+ from mkdocs.structure.files import File
6
+
7
+ STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
8
+ ASSET_NAMES = ("nested-tabs.js", "nested-tabs.css")
9
+
10
+
11
+ class NestedTabsPlugin(BasePlugin):
12
+ """Shows every navigation.tabs category with all its child pages at once,
13
+ instead of Material's hover-only dropdown. See README for background —
14
+ this fills a gap explicitly declined in squidfunk/mkdocs-material#4765.
15
+ """
16
+
17
+ config_scheme = (
18
+ # v0.1: static breakpoint/behavior, matching Material's own tabs
19
+ # breakpoint (76.234375em). Making these configurable needs the
20
+ # static JS/CSS to be templated per-build rather than served as
21
+ # fixed package assets — planned, not yet implemented.
22
+ ("enabled", config_options.Type(bool, default=True)),
23
+ )
24
+
25
+ def on_files(self, files, config):
26
+ if not self.config["enabled"]:
27
+ return files
28
+ # File(path, src_dir, dest_dir, ...) mirrors the same relative `path`
29
+ # under both src_dir and dest_dir — keeping the assets flat (no
30
+ # subfolder) in mkdocs_nested_tabs/static/ avoids any src/dest
31
+ # mismatch, at the cost of sitting at the built site's root rather
32
+ # than under its own assets/ subfolder. Confirmed working against
33
+ # tests/fixture_site (see tests/test_plugin.py).
34
+ for name in ASSET_NAMES:
35
+ files.append(
36
+ File(name, src_dir=STATIC_DIR, dest_dir=config["site_dir"], use_directory_urls=False)
37
+ )
38
+ return files
39
+
40
+ def on_config(self, config):
41
+ if not self.config["enabled"]:
42
+ return config
43
+ config["extra_javascript"] = ["nested-tabs.js", *config["extra_javascript"]]
44
+ config["extra_css"] = ["nested-tabs.css", *config["extra_css"]]
45
+ return config
@@ -0,0 +1,91 @@
1
+ /* True desktop only (matches Material's own navigation.tabs breakpoint):
2
+ .mdx-nested-tabs replaces the native tab bar there — each category's name
3
+ is already its own column header in this row, so keeping both would show
4
+ every category name twice, stacked on top of itself. Below this width,
5
+ Material's native tabs (or the hamburger drawer) still apply unmodified. */
6
+ @media screen and (min-width: 76.234375em) {
7
+ .md-tabs {
8
+ display: none;
9
+ }
10
+ }
11
+
12
+ /* .mdx-nested-tabs is inserted as a DOM child of .md-header, right after
13
+ .md-tabs (see nested-tabs.js) — so it's a genuine part of the header, not
14
+ a separate bar bolted on below: it rides along with .md-header's own
15
+ sticky positioning and background automatically. Transparent background
16
+ for the same reason .md-tabs is transparent: lets the header's own
17
+ background/artwork show through instead of drawing a second, visually
18
+ separate box. */
19
+ .mdx-nested-tabs {
20
+ display: none;
21
+ }
22
+
23
+ @media screen and (min-width: 76.234375em) {
24
+ .mdx-nested-tabs {
25
+ display: block;
26
+ background-color: transparent;
27
+ }
28
+
29
+ .mdx-nested-tabs__list {
30
+ display: flex;
31
+ list-style: none;
32
+ margin: 0 0.2rem;
33
+ padding: 0.5rem 0;
34
+ gap: 1.4rem;
35
+ }
36
+
37
+ .mdx-nested-tabs__group {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 0.15rem;
41
+ }
42
+
43
+ .mdx-nested-tabs__label {
44
+ font-size: 0.64rem;
45
+ font-weight: 700;
46
+ text-transform: uppercase;
47
+ letter-spacing: 0.03em;
48
+ /* Falls back to Material's own faded-foreground variable so this reads
49
+ reasonably on any palette out of the box; a consumer can override
50
+ --md-nested-tabs-label-color directly without touching this file. */
51
+ color: var(--md-nested-tabs-label-color, var(--md-default-fg-color--light));
52
+ }
53
+
54
+ /* A category with a third nesting level (see nested-tabs.js) renders as a
55
+ single link instead of a label + page list — needs its own hover/active
56
+ state, unlike the other groups' inert <span> labels. */
57
+ .mdx-nested-tabs__label--link {
58
+ text-decoration: none;
59
+ }
60
+
61
+ .mdx-nested-tabs__label--link:hover,
62
+ .mdx-nested-tabs__label--link:focus-visible {
63
+ color: var(--md-accent-fg-color);
64
+ text-decoration: underline;
65
+ }
66
+
67
+ .mdx-nested-tabs__pages {
68
+ display: flex;
69
+ list-style: none;
70
+ margin: 0;
71
+ padding: 0;
72
+ gap: 0.7rem;
73
+ }
74
+
75
+ .mdx-nested-tabs__link {
76
+ font-size: 0.7rem;
77
+ color: var(--md-nested-tabs-link-color, var(--md-default-fg-color));
78
+ text-decoration: none;
79
+ }
80
+
81
+ .mdx-nested-tabs__link:hover,
82
+ .mdx-nested-tabs__link:focus-visible {
83
+ color: var(--md-accent-fg-color);
84
+ text-decoration: underline;
85
+ }
86
+
87
+ .mdx-nested-tabs__link--active {
88
+ color: var(--md-accent-fg-color);
89
+ font-weight: 700;
90
+ }
91
+ }
@@ -0,0 +1,142 @@
1
+ (function () {
2
+ // Desktop-only nav row that replaces Material's native tab bar: every
3
+ // navigation.tabs category shown at once with all of its child pages
4
+ // listed underneath, instead of only being reachable via Material's
5
+ // hover-triggered tab dropdown (a gap explicitly declined upstream —
6
+ // see squidfunk/mkdocs-material#4765). Built by reading Material's own
7
+ // primary sidebar nav, which already contains the full site tree (every
8
+ // section, not just the active one — Material only flags the active
9
+ // top-level section with --section; every other one just gets --nested,
10
+ // regardless of which page you're on) rather than requiring a
11
+ // hand-maintained config list, so this stays in sync with mkdocs.yml's
12
+ // nav: block automatically.
13
+ //
14
+ // A category with a third nesting level (a sub-category with its own
15
+ // children, rather than a flat list of pages) doesn't fit the
16
+ // "category + pages" shape this row renders — those fall back to a
17
+ // single link, using the first leaf page found inside them as the
18
+ // target (usually an "index"/"All" overview page).
19
+
20
+ function buildNestedTabs() {
21
+ const primaryNav = document.querySelector(
22
+ '[data-md-component="sidebar"][data-md-type="navigation"] .md-nav--primary'
23
+ );
24
+ if (!primaryNav) return null;
25
+
26
+ const topList = primaryNav.querySelector(":scope > ul.md-nav__list");
27
+ if (!topList) return null;
28
+
29
+ const nav = document.createElement("nav");
30
+ nav.className = "mdx-nested-tabs";
31
+ nav.setAttribute("aria-label", "Categories");
32
+
33
+ // .md-tabs wraps its own list in a bare .md-grid div for the same
34
+ // centered max-width alignment as .md-header__inner (logo/title/search)
35
+ // — match that structure so this row lines up with the rest of the
36
+ // header instead of running edge-to-edge.
37
+ const grid = document.createElement("div");
38
+ grid.className = "md-grid mdx-nested-tabs__grid";
39
+
40
+ const list = document.createElement("ul");
41
+ list.className = "mdx-nested-tabs__list";
42
+
43
+ topList.querySelectorAll(":scope > li.md-nav__item--nested").forEach(function (section) {
44
+ const labelEl = section.querySelector(":scope > label.md-nav__link, :scope > a.md-nav__link");
45
+ const nestedNav = section.querySelector(":scope > nav.md-nav");
46
+ if (!labelEl || !nestedNav) return;
47
+
48
+ const categoryLabel = labelEl.querySelector(".md-ellipsis")
49
+ ? labelEl.querySelector(".md-ellipsis").textContent.trim()
50
+ : labelEl.textContent.trim();
51
+
52
+ const childItems = Array.from(nestedNav.querySelectorAll(":scope > ul.md-nav__list > li.md-nav__item"));
53
+ const pageLinks = [];
54
+ let flat = true;
55
+ for (const pageItem of childItems) {
56
+ // A real page item links directly to itself; a further-nested
57
+ // sub-category only has a label + its own nested nav, same shape
58
+ // as this top-level section.
59
+ const link = pageItem.querySelector(":scope > a.md-nav__link");
60
+ if (!link) {
61
+ flat = false;
62
+ break;
63
+ }
64
+ pageLinks.push(link);
65
+ }
66
+
67
+ const group = document.createElement("li");
68
+ group.className = "mdx-nested-tabs__group";
69
+
70
+ if (flat && pageLinks.length > 0) {
71
+ const label = document.createElement("span");
72
+ label.className = "mdx-nested-tabs__label";
73
+ label.textContent = categoryLabel;
74
+ group.appendChild(label);
75
+
76
+ const pages = document.createElement("ul");
77
+ pages.className = "mdx-nested-tabs__pages";
78
+ pageLinks.forEach(function (link) {
79
+ const item = document.createElement("li");
80
+ const a = document.createElement("a");
81
+ a.className = "mdx-nested-tabs__link";
82
+ a.href = link.getAttribute("href");
83
+ a.textContent = link.querySelector(".md-ellipsis")
84
+ ? link.querySelector(".md-ellipsis").textContent.trim()
85
+ : link.textContent.trim();
86
+ if (link.classList.contains("md-nav__link--active")) {
87
+ a.classList.add("mdx-nested-tabs__link--active");
88
+ a.setAttribute("aria-current", "page");
89
+ }
90
+ item.appendChild(a);
91
+ pages.appendChild(item);
92
+ });
93
+ group.appendChild(pages);
94
+ } else {
95
+ const overviewLink = nestedNav.querySelector(
96
+ ":scope > ul.md-nav__list > li.md-nav__item > a.md-nav__link"
97
+ );
98
+ if (!overviewLink) return;
99
+
100
+ const label = document.createElement("a");
101
+ label.className = "mdx-nested-tabs__label mdx-nested-tabs__label--link";
102
+ label.href = overviewLink.getAttribute("href");
103
+ label.textContent = categoryLabel;
104
+ if (overviewLink.classList.contains("md-nav__link--active")) {
105
+ label.classList.add("mdx-nested-tabs__link--active");
106
+ label.setAttribute("aria-current", "page");
107
+ }
108
+ group.appendChild(label);
109
+ }
110
+
111
+ list.appendChild(group);
112
+ });
113
+
114
+ if (!list.children.length) return null;
115
+
116
+ grid.appendChild(list);
117
+ nav.appendChild(grid);
118
+ return nav;
119
+ }
120
+
121
+ // .md-tabs lives inside .md-header (not below it, despite how it reads
122
+ // visually), so inserting right after it via insertAdjacentElement makes
123
+ // this row a header child too — it rides along with .md-header's own
124
+ // sticky positioning and decorative background for free, no separate
125
+ // sticky/offset math needed here.
126
+ function render() {
127
+ const existing = document.querySelector(".mdx-nested-tabs");
128
+ if (existing) existing.remove();
129
+
130
+ const nestedTabs = buildNestedTabs();
131
+ const tabs = document.querySelector(".md-tabs");
132
+ if (nestedTabs && tabs) {
133
+ tabs.insertAdjacentElement("afterend", nestedTabs);
134
+ }
135
+ }
136
+
137
+ if (window.document$) {
138
+ window.document$.subscribe(render);
139
+ } else {
140
+ document.addEventListener("DOMContentLoaded", render);
141
+ }
142
+ })();
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-nested-tabs
3
+ Version: 0.1.0
4
+ Summary: Display two levels of navigation.tabs instead of Material's hover-only dropdown.
5
+ Author-email: Luka Sherman <lukawritecode@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/luka-sherman/mkdocs-nested-tabs
8
+ Project-URL: Repository, https://github.com/luka-sherman/mkdocs-nested-tabs
9
+ Project-URL: Issues, https://github.com/luka-sherman/mkdocs-nested-tabs/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: MkDocs
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Documentation
16
+ Classifier: Topic :: Software Development :: Documentation
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: mkdocs>=1.5
21
+ Requires-Dist: mkdocs-material>=9.0
22
+ Dynamic: license-file
23
+
24
+ # mkdocs-nested-tabs
25
+
26
+ Shows two hierarchy levels of `navigation.tabs` - one parent level, with it's children
27
+ displayed below it. An alternative to Material for MkDocs' native hover-triggered
28
+ tab dropdown (one category's children at a time).
29
+
30
+ ## Status
31
+
32
+ Early scaffold, ported and generalized from a working implementation. Not yet published.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install mkdocs-nested-tabs # not yet published
38
+ ```
39
+
40
+ ```yaml
41
+ # mkdocs.yml
42
+ theme:
43
+ name: material
44
+ features:
45
+ - navigation.tabs
46
+
47
+ plugins:
48
+ - nested-tabs
49
+ ```
50
+
51
+ Requires `navigation.tabs` to be enabled — this plugin replaces that
52
+ feature's tab bar on desktop widths (≥76.234375em, matching Material's own
53
+ breakpoint), it doesn't work alongside a site with tabs disabled.
54
+
55
+ ## How it works
56
+
57
+ Reads Material's own primary sidebar nav at runtime (which already contains
58
+ the full site tree — every category, not just the active one) and builds a
59
+ second row inserted directly into `.md-header`, right after `.md-tabs`, so
60
+ it inherits the header's own sticky positioning and background for free. A
61
+ category with a third nesting level (a sub-category with its own children,
62
+ rather than a flat list of pages) doesn't fit the "category + pages" shape
63
+ this renders, so it falls back to a single link using the first leaf page
64
+ found inside it.
65
+
66
+ ## Config
67
+
68
+ ```yaml
69
+ plugins:
70
+ - nested-tabs:
71
+ enabled: true # default
72
+ ```
73
+
74
+ A configurable breakpoint and an option to keep Material's native tabs
75
+ alongside this row instead of hiding them. Both need the static JS/CSS to
76
+ be templated per-build rather than shipped as fixed package assets.
77
+
78
+ ## Theming
79
+
80
+ Falls back to Material's own `--md-default-fg-color`/`--md-accent-fg-color`
81
+ so it looks reasonable on any palette out of the box. Override via:
82
+
83
+ ```css
84
+ :root {
85
+ --md-nested-tabs-label-color: ...;
86
+ --md-nested-tabs-link-color: ...;
87
+ }
88
+ ```
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ python3 -m venv .venv && source .venv/bin/activate
94
+ pip install -e . mkdocs-material pytest
95
+ python -m pytest tests/
96
+ ```
97
+
98
+ Manual check: `cd tests/fixture_site && mkdocs serve`, then open the site
99
+ and confirm the nested-tabs row renders at desktop width.
@@ -0,0 +1,10 @@
1
+ mkdocs_nested_tabs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mkdocs_nested_tabs/plugin.py,sha256=Z0rAr5Epjc2GuhYKKqGHYcapDmtBBnufhbiNQd8gqOk,1903
3
+ mkdocs_nested_tabs/static/nested-tabs.css,sha256=e_XiYSSD1ROefbGJkjZRgAROp6gtYXMGnIr0e2PTrpU,2742
4
+ mkdocs_nested_tabs/static/nested-tabs.js,sha256=cNLAm0MxKRUTLg1Oiiuud6y5Vu5mtAcCAqkj5AcdoUs,5835
5
+ mkdocs_nested_tabs-0.1.0.dist-info/licenses/LICENSE,sha256=8eSg7z3uZ_8PXMr30y4f3GlWv6ND5QUAIOB3eccrtc0,1069
6
+ mkdocs_nested_tabs-0.1.0.dist-info/METADATA,sha256=42utYNzM8H8Ivr2t5EQWBQ6gdt-BCdBLQHhtMZbR_oQ,3068
7
+ mkdocs_nested_tabs-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ mkdocs_nested_tabs-0.1.0.dist-info/entry_points.txt,sha256=SxMM-nl2YLrXFkajtHvmf_hhUfX706IzuPuoCPPJU_k,74
9
+ mkdocs_nested_tabs-0.1.0.dist-info/top_level.txt,sha256=OXfvOxPrKbR12BOZ1B_7OVZd6hBBgEgktFW1wTsHtms,19
10
+ mkdocs_nested_tabs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ nested-tabs = mkdocs_nested_tabs.plugin:NestedTabsPlugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luka Sherman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mkdocs_nested_tabs