Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11,565 changes: 11,565 additions & 0 deletions great_docs/_icons.py

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions great_docs/assets/great-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4968,6 +4968,53 @@ html[data-bs-theme="dark"] {
}
}

/* ── Navigation Icons (Lucide SVG) ──────────────────────────────────────── */

/* Shared icon base — applies to both navbar and sidebar icons */
.gd-nav-icon {
flex-shrink: 0;
margin-right: 0.4em;
vertical-align: -0.15em; /* optical alignment with text baseline */
opacity: 0.7;
transition: opacity 0.15s ease;
}

/* Navbar icons */
.navbar .nav-link > .menu-text > .gd-nav-icon {
width: 16px;
height: 16px;
}

.navbar .nav-link:hover > .menu-text > .gd-nav-icon {
opacity: 1;
}

/* Sidebar icons */
#quarto-sidebar .menu-text > .gd-nav-icon,
#quarto-sidebar .sidebar-item-text > .gd-nav-icon {
width: 15px;
height: 15px;
}

#quarto-sidebar .sidebar-item:hover .gd-nav-icon {
opacity: 1;
}

/* Active sidebar item — full opacity */
#quarto-sidebar .sidebar-link.active > .menu-text > .gd-nav-icon,
#quarto-sidebar a.sidebar-item-text.sidebar-link.active > .gd-nav-icon {
opacity: 1;
}

/* Section header icons — slightly larger */
#quarto-sidebar .sidebar-item-section > .sidebar-item-container .gd-nav-icon {
width: 16px;
height: 16px;
}

/* Dark mode — icon color inherits from text (stroke: currentColor) */
/* No extra rules needed because Lucide uses stroke="currentColor" */


/*-- scss:functions --*/

Expand Down
90 changes: 90 additions & 0 deletions great_docs/assets/nav-icons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Navigation Icons for Great Docs
*
* Reads a JSON icon map from a <script id="gd-nav-icons-data"> element
* and prepends inline SVG icons to matching navbar and sidebar entries.
*
* The icon map is emitted by the Python build pipeline and has the shape:
* { "navbar": { "Label": "<svg .../>", ... },
* "sidebar": { "Label": "<svg .../>", ... } }
*/
(function () {
"use strict";

/**
* Inject an SVG icon before the text of a navigation link.
*
* @param {Element} menuText - The .menu-text span element.
* @param {string} svgHtml - The inline SVG markup to prepend.
*/
function injectIcon(menuText, svgHtml) {
// Avoid double-injection on re-runs
if (menuText.querySelector(".gd-nav-icon")) return;

// Create a temporary container to parse the SVG string
var wrapper = document.createElement("span");
wrapper.innerHTML = svgHtml;
var svg = wrapper.firstElementChild;
if (!svg) return;

menuText.insertBefore(svg, menuText.firstChild);
}

/**
* Process a set of navigation items, matching by their text content.
*
* @param {string} selector - CSS selector to find .menu-text elements.
* @param {Object} mapping - Label -> SVG HTML mapping.
*/
function processNavItems(selector, mapping) {
if (!mapping || typeof mapping !== "object") return;

var items = document.querySelectorAll(selector);
items.forEach(function (el) {
// Get the trimmed text content (may include child node text)
var text = el.textContent.trim();
if (mapping[text]) {
injectIcon(el, mapping[text]);
}
});
}

function run() {
var dataEl = document.getElementById("gd-nav-icons-data");
if (!dataEl) return;

var iconMap;
try {
iconMap = JSON.parse(dataEl.textContent);
} catch (_) {
return;
}
if (!iconMap) return;

// Process navbar items: look for .navbar .menu-text spans
processNavItems(".navbar .nav-link > .menu-text", iconMap.navbar || {});

// Process sidebar items: look for .sidebar .menu-text spans
// This covers sidebar section titles and individual items
processNavItems(
"#quarto-sidebar .sidebar-item .menu-text",
iconMap.sidebar || {}
);

// Also handle sidebar section headers (the collapsible section titles)
processNavItems(
"#quarto-sidebar .sidebar-item-section .sidebar-item-text",
iconMap.sidebar || {}
);
}

// Run after DOM is ready so that other scripts (e.g. sidebar-wrap.js)
// that also use DOMContentLoaded have already processed the sidebar.
// Listeners fire in registration order, and sidebar-wrap.js is loaded
// earlier in include-after-body, so its handler runs first.
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", run);
} else {
run();
}
})();
93 changes: 65 additions & 28 deletions great_docs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@
# str: preset name (applies to all pages)
# dict: {"preset": str, "pages": "all"|"homepage"}
"content_style": None,
# Navigation icons (Lucide icon set)
# Prepend icons to sidebar and navbar navigation entries.
# None/False: disabled (default)
# dict: {"navbar": {"Label": "icon-name"}, "sidebar": {"Label": "icon-name"}}
"nav_icons": None,
# Back-to-top floating button
# True (default): show back-to-top button on all pages
# False: disable back-to-top button
Expand Down Expand Up @@ -510,8 +515,7 @@ def user_guide_dir(self) -> str | None:
def reference_enabled(self) -> bool:
"""Whether API reference generation is enabled.

Returns ``False`` when the config contains ``reference: false``.
Defaults to ``True``.
Returns `False` when the config contains `reference: false`. Defaults to `True`.
"""
val = self.get("reference", [])
if val is False:
Expand Down Expand Up @@ -555,8 +559,8 @@ def reference(self) -> list[dict[str, Any]]:
def reference_title(self) -> str | None:
"""Get the custom API reference title, if set.

Supports ``reference: {title: "Custom Title"}`` in great-docs.yml.
Returns ``None`` when no custom title is configured.
Supports `reference: {title: "Custom Title"}` in great-docs.yml. Returns `None` when no
custom title is configured.
"""
val = self.get("reference", [])
if isinstance(val, dict):
Expand All @@ -567,9 +571,8 @@ def reference_title(self) -> str | None:
def reference_desc(self) -> str | None:
"""Get the custom API reference description, if set.

Supports ``reference: {desc: "Description text..."}`` in great-docs.yml.
The description appears below the reference page heading.
Returns ``None`` when no description is configured.
Supports `reference: {desc: "Description text..."}` in great-docs.yml. Returns `None` when
no description is configured.
"""
val = self.get("reference", [])
if isinstance(val, dict):
Expand Down Expand Up @@ -654,9 +657,8 @@ def logo(self) -> dict[str, Any] | None:
Returns
-------
dict | None
Normalized logo dict with at least ``light`` key, or ``None`` if
no logo is configured. A bare string in ``great-docs.yml`` is
expanded to ``{"light": "<path>", "dark": "<path>"}``.
Normalized logo dict with at least `light` key, or `None` if no logo is configured. A
bare string in `great-docs.yml` is expanded to `{"light": "<path>", "dark": "<path>"}`.
"""
raw = self.get("logo")
if raw is None:
Expand All @@ -679,8 +681,7 @@ def logo_show_title(self) -> bool:
def hero_enabled(self) -> bool:
"""Whether the hero section is enabled.

Auto-enables when a logo is configured and ``hero`` is not
explicitly set to ``False``.
Auto-enables when a logo is configured and `hero` is not explicitly set to `False`.
"""
raw = self.get("hero")
if raw is False:
Expand Down Expand Up @@ -718,10 +719,9 @@ def hero(self) -> dict[str, Any]:
def hero_logo(self) -> str | dict | None | bool:
"""Get the explicit hero logo config.

Returns the hero-specific logo value only. Returns ``False``
when explicitly suppressed, ``None`` when not configured.
The full fallback chain (auto-detected hero logos, navbar logo)
is handled in ``core._build_hero_section``.
Returns the hero-specific logo value only. Returns `False` when explicitly suppressed,
`None` when not configured. The full fallback chain (auto-detected hero logos, navbar logo)
is handled in `core._build_hero_section`.
"""
hero = self.hero
val = hero.get("logo") if hero else None
Expand All @@ -741,7 +741,7 @@ def hero_logo_height(self) -> str:
def hero_name(self) -> str | None:
"""Get the hero name, falling back to display_name.

Returns ``None`` when explicitly suppressed (``false``).
Returns `None` when explicitly suppressed (`false`).
"""
hero = self.hero
val = hero.get("name") if hero else None
Expand All @@ -755,8 +755,8 @@ def hero_name(self) -> str | None:
def hero_tagline(self) -> str | None:
"""Get the hero tagline.

Returns ``None`` when explicitly suppressed (``false``).
Auto-resolved from package metadata in core.py.
Returns `None` when explicitly suppressed (`false`). Auto-resolved from package metadata in
core.py.
"""
hero = self.hero
val = hero.get("tagline") if hero else None
Expand All @@ -768,8 +768,8 @@ def hero_tagline(self) -> str | None:
def hero_badges(self) -> str | list | None:
"""Get the hero badges config.

Returns ``"auto"`` (default, extract from README), an explicit list
of badge dicts, or ``None`` (disabled).
Returns `"auto"` (default, extract from README), an explicit list of badge dicts, or `None`
(disabled).
"""
hero = self.hero
val = hero.get("badges") if hero else None
Expand All @@ -787,9 +787,8 @@ def favicon(self) -> dict[str, Any] | None:
Returns
-------
dict | None
Normalized favicon dict with at least ``icon`` key, or ``None``
if no favicon is explicitly configured (auto-generation may still
produce one from the logo).
Normalized favicon dict with at least `icon` key, or `None` if no favicon is explicitly
configured (auto-generation may still produce one from the logo).
"""
raw = self.get("favicon")
if raw is None:
Expand All @@ -807,8 +806,8 @@ def announcement(self) -> dict[str, Any] | None:
Returns
-------
dict | None
Normalized dict with keys: content, type, dismissable, url.
Returns None if no announcement is configured.
Normalized dict with keys: content, type, dismissable, url. Returns `None` if no
announcement is configured.
"""
raw = self.get("announcement")
if raw is None or raw is False:
Expand All @@ -832,8 +831,8 @@ def announcement(self) -> dict[str, Any] | None:
def include_in_header(self) -> list[dict[str, str]]:
"""Get the normalized include-in-header entries.

Returns a list of Quarto-compatible include-in-header items
(each a dict with either a "text" or "file" key).
Returns a list of Quarto-compatible include-in-header items (each a dict with either a
"text" or "file" key).
"""
raw = self.get("include_in_header", [])
if raw is None:
Expand All @@ -850,6 +849,44 @@ def include_in_header(self) -> list[dict[str, str]]:
return result
return []

@property
def nav_icons(self) -> dict[str, dict[str, str]] | None:
"""Get the normalized navigation icons configuration.

Returns
-------
dict | None
A dict with optional `navbar` and `sidebar` keys, each mapping navigation label text to
a Lucide icon name. Returns `None` when not configured.
"""
raw = self.get("nav_icons")
if raw is None or raw is False:
return None
if isinstance(raw, dict):
result: dict[str, dict[str, str]] = {}
for scope in ("navbar", "sidebar"):
mapping = raw.get(scope)
if isinstance(mapping, dict):
result[scope] = {str(k): str(v) for k, v in mapping.items()}
return result if result else None
return None

@property
def nav_icons_navbar(self) -> dict[str, str]:
"""Get the navbar icon mapping (label -> icon name)."""
icons = self.nav_icons
if icons is None:
return {}
return icons.get("navbar", {})

@property
def nav_icons_sidebar(self) -> dict[str, str]:
"""Get the sidebar icon mapping (label -> icon name)."""
icons = self.nav_icons
if icons is None:
return {}
return icons.get("sidebar", {})

@property
def attribution(self) -> bool:
"""Whether to show Great Docs attribution in the footer."""
Expand Down
Loading
Loading