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
57 changes: 57 additions & 0 deletions _static/custom.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
/* RTD search trigger: center the bar in the header and fix vertical alignment.
Without data-md-component="search", sphinx_immaterial does not manage this element,
so positioning and sizing must be set explicitly.
position:absolute + left:50%/translateX(-50%) centers the bar relative to the
header regardless of where the flex siblings land. */
.md-header__inner {
position: relative;
}
.md-header .md-search {
display: flex;
align-items: center;
width: 20rem;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: auto;
}

.md-header .md-search .md-search__inner,
.md-header .md-search .md-search__form {
display: flex;
align-items: center;
width: 100%;
}

.md-header .md-search .md-search__input {
width: 100%;
}

/* Sync RTD Addons search modal colors with sphinx_immaterial's slate (dark) scheme.
Variables must be set directly on the readthedocs-search element, not on body —
light-DOM styles on the host element override the shadow DOM's :host rules,
whereas inherited values from body lose to :host. */
body[data-md-color-scheme="slate"] readthedocs-search {
--readthedocs-search-color: var(--md-default-fg-color);
--readthedocs-search-link-color: var(--md-default-fg-color);
--readthedocs-search-content-background-color: var(--md-default-bg-color);
--readthedocs-search-input-background-color: var(--md-default-bg-color--light);
--readthedocs-search-footer-background-color: var(--md-default-bg-color--light);
--readthedocs-search-footer-color: var(--md-default-fg-color);
--readthedocs-search-footer-code-background-color: var(--md-default-bg-color--lighter);
--readthedocs-search-content-border-color: var(--md-default-fg-color--lightest);
--readthedocs-search-filters-background-color: var(--md-default-bg-color--light);
--readthedocs-search-filters-border-color: var(--md-default-fg-color--lightest);
--readthedocs-search-result-border-color: var(--md-default-fg-color--lightest);
--readthedocs-search-footer-code-border-color: var(--md-default-fg-color--lightest);
--readthedocs-search-result-active-background-color: var(--md-default-bg-color--light);
--readthedocs-search-result-color: var(--md-default-fg-color--light);
--readthedocs-search-result-heading-color: var(--md-default-fg-color);
--readthedocs-search-result-subheading-color: var(--md-default-fg-color--light);
--readthedocs-search-result-icon-color: var(--md-default-fg-color--light);
--readthedocs-search-badge-color: var(--md-default-fg-color);
--readthedocs-search-badge-background-color: var(--md-default-bg-color--light);
--readthedocs-search-backdrop-color: rgba(0, 0, 0, 0.7);
}

.o-tooltip--left:after{
font-size: 0.8rem;
}
Expand Down
200 changes: 73 additions & 127 deletions _templates/partials/search.html
Original file line number Diff line number Diff line change
@@ -1,153 +1,95 @@
{#- Custom search that uses ReadTheDocs server-side API for cross-project searching -#}
{#- On RTD: forward search interactions to RTD Addons, which provides cross-project
search across all flux-framework subprojects natively. Off RTD: fall back to
sphinx_immaterial's default client-side search. -#}
{% if on_rtd %}
{# On ReadTheDocs, use server-side search API which supports cross-project search #}
<div class="md-search" data-md-component="search" role="dialog">
<div class="md-search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search" id="rtd-search-form">
<input type="text" name="q" class="md-search__input" placeholder="Search across all Flux docs" aria-label="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<form class="md-search__form" name="search">
<input type="text"
class="md-search__input"
placeholder="Search across all Flux docs"
aria-label="Search"
autocapitalize="off"
autocorrect="off"
autocomplete="off"
spellcheck="false"
style="cursor: pointer"
readonly>
<label class="md-search__icon md-icon" for="__search">
{% set icon = config.theme.icon.search or "material/magnify" %}
{% include ".icons/" ~ icon ~ ".svg" %}
{% set icon = config.theme.icon.previous or "material/arrow-left" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
{% set icon = config.theme.icon.close or "material/close" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Type to search across all Flux documentation
</div>
<ol class="md-search-result__list" role="presentation" id="rtd-search-results"></ol>
</div>
</div>
</div>
</div>
</div>

<script>
(function() {
const form = document.getElementById('rtd-search-form');
const input = form.querySelector('input[name="q"]');
const resultsContainer = document.getElementById('rtd-search-results');
const metaContainer = resultsContainer.previousElementSibling;
let searchTimeout;

// ReadTheDocs API endpoint - use relative path to avoid CORS issues
// RTD proxies the API at /_/api/v3/ on the same domain
const RTD_SEARCH_API = '/_/api/v3/search/';
const PROJECT_SLUG = 'flux-framework';
const SEARCH_SUBPROJECTS = true;

input.addEventListener('input', function(e) {
clearTimeout(searchTimeout);
const query = e.target.value.trim();

if (query.length < 2) {
metaContainer.textContent = 'Type to search across all Flux documentation';
resultsContainer.innerHTML = '';
return;
}

metaContainer.textContent = 'Searching...';

searchTimeout = setTimeout(() => {
searchRTD(query);
}, 300);
});

async function searchRTD(query) {
try {
let searchUrl = `${RTD_SEARCH_API}?q=${encodeURIComponent(query)}&project=${PROJECT_SLUG}`;
if (SEARCH_SUBPROJECTS) {
searchUrl += '&subprojects=true';
}

const response = await fetch(searchUrl);
const data = await response.json();

displayResults(data.results || [], query);
} catch (err) {
console.error('Search error:', err);
metaContainer.textContent = 'Search error occurred';
resultsContainer.innerHTML = '';
}
// Dispatch the RTD Addons event that opens the cross-project search modal.
// The input has no data-md-component attributes so sphinx_immaterial ignores it;
// all search interactions are forwarded to RTD Addons instead.
const form = document.currentScript.previousElementSibling;
const input = form.querySelector('input');
function showRTDSearch(e) {
e.preventDefault();
document.dispatchEvent(new CustomEvent('readthedocs-search-show', { bubbles: true }));
}

function displayResults(results, query) {
if (results.length === 0) {
metaContainer.textContent = `No results found for "${query}"`;
resultsContainer.innerHTML = '';
return;
}

metaContainer.textContent = `${results.length} result${results.length !== 1 ? 's' : ''} found`;

// Sort by relevance score if available
results.sort((a, b) => (b.score || 0) - (a.score || 0));

resultsContainer.innerHTML = results.slice(0, 20).map(result => {
const project = result.project || 'flux-framework';
const projectLabel = project.replace('flux-', '');

return `
<li class="md-search-result__item">
<a href="${result.link || result.path}" class="md-search-result__link" tabindex="-1">
<article class="md-search-result__article md-search-result__article--document">
<h1 class="md-search-result__title">${highlightText(result.title, query)}</h1>
<p class="md-search-result__teaser">
<span class="md-search-result__project">[${projectLabel}]</span>
${result.blocks && result.blocks[0] ? highlightText(result.blocks[0].content.substring(0, 150), query) + '...' : ''}
</p>
</article>
</a>
</li>
`;
}).join('');
input.addEventListener('focus', showRTDSearch);
input.addEventListener('click', showRTDSearch);

// Fix dark mode in the RTD Addons search modal. The shadow DOM uses hardcoded
// background colors for some elements (e.g. .filters-title) that our external
// CSS custom properties cannot override. We inject a <style> into the shadow
// root; CSS custom properties DO pierce the shadow boundary, so we can still
// reference our --readthedocs-search-* variables from the injected style.
const DARK_STYLE_ID = 'flux-rtd-dark';
function injectDarkStyle(shadowRoot) {
if (shadowRoot.getElementById(DARK_STYLE_ID)) return;
const style = document.createElement('style');
style.id = DARK_STYLE_ID;
style.textContent = [
/* filters-title floats as a label over the border — needs content bg */
'.filters-title { background: var(--readthedocs-search-content-background-color) !important; }',
].join('\n');
shadowRoot.appendChild(style);
}

function highlightText(text, query) {
if (!text) return '';
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
function removeDarkStyle(shadowRoot) {
const existing = shadowRoot.getElementById(DARK_STYLE_ID);
if (existing) existing.remove();
}

form.addEventListener('submit', function(e) {
e.preventDefault();
function syncDarkMode() {
const rtdSearch = document.querySelector('readthedocs-search');
if (!rtdSearch || !rtdSearch.shadowRoot) return;
const isDark = document.body.getAttribute('data-md-color-scheme') === 'slate';
isDark ? injectDarkStyle(rtdSearch.shadowRoot) : removeDarkStyle(rtdSearch.shadowRoot);
}
// Watch for the readthedocs-search element to be added to the DOM.
const domObserver = new MutationObserver(function(_, obs) {
if (document.querySelector('readthedocs-search')) {
obs.disconnect();
syncDarkMode();
// Re-sync whenever the user toggles the color scheme.
new MutationObserver(syncDarkMode).observe(document.body, {
attributes: true, attributeFilter: ['data-md-color-scheme']
});
}
});
domObserver.observe(document.body, { childList: true, subtree: true });
})();
</script>

<style>
.md-search-result__project {
display: inline-block;
font-weight: bold;
font-size: 0.7rem;
padding: 0.1em 0.4em;
margin-right: 0.5em;
background: var(--md-primary-fg-color);
color: var(--md-primary-bg-color);
border-radius: 0.2rem;
}
.md-search-result__teaser mark {
background: var(--md-accent-fg-color);
color: var(--md-accent-bg-color);
}
</style>
{% else %}
{# Fallback to theme's default search when not on RTD #}
{# Fallback to sphinx_immaterial's default search for local builds #}
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="{{ lang.t('search.placeholder') }}" placeholder="{{ lang.t('search.placeholder') }}" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<input type="text" class="md-search__input" name="query"
aria-label="{{ lang.t('search.placeholder') }}"
placeholder="{{ lang.t('search.placeholder') }}"
autocapitalize="off" autocorrect="off" autocomplete="off"
spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
{% set icon = config.theme.icon.search or "material/magnify" %}
{% include ".icons/" ~ icon ~ ".svg" %}
Expand All @@ -156,12 +98,16 @@ <h1 class="md-search-result__title">${highlightText(result.title, query)}</h1>
</label>
<nav class="md-search__options" aria-label="{{ lang.t('search') }}">
{% if "search.share" in features %}
<a href="javascript:void(0)" class="md-search__icon md-icon" title="{{ lang.t('search.share') }}" aria-label="{{ lang.t('search.share') }}" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<a href="javascript:void(0)" class="md-search__icon md-icon"
title="{{ lang.t('search.share') }}" aria-label="{{ lang.t('search.share') }}"
data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
{% set icon = config.theme.icon.share or "material/share-variant" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
{% endif %}
<button type="reset" class="md-search__icon md-icon" title="{{ lang.t('search.reset') }}" aria-label="{{ lang.t('search.reset') }}" tabindex="-1">
<button type="reset" class="md-search__icon md-icon"
title="{{ lang.t('search.reset') }}" aria-label="{{ lang.t('search.reset') }}"
tabindex="-1">
{% set icon = config.theme.icon.close or "material/close" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</button>
Expand Down
Loading