diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..61b7b696 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to FreeUnit Documentation are documented in this file. + +## [1.1.0] - 2026-05-05 + +### Added +- Full-text search with lunr.js library +- Global search box in sidebar with keyboard shortcut (Cmd+K / Ctrl+K) +- Search autocomplete with popular page suggestions +- Keyboard navigation (Enter to search, Escape to close) +- Responsive dark/light mode search UI +- Search index generation via Sphinx extension + +--- + +## [1.0.0] - 2026-05-05 + +### Added +- Initial FreeUnit documentation website +- Comprehensive guides for installation, configuration, and usage +- Control API documentation +- Status API documentation +- Scripting and security sections +- Community resources and troubleshooting guides +- Docker and deployment examples +- Theme and static assets +- Build system with Sphinx and Make + diff --git a/source/conf.py b/source/conf.py index 398f13e8..37efa1f1 100644 --- a/source/conf.py +++ b/source/conf.py @@ -39,5 +39,5 @@ suppress_warnings = ['misc.highlighting_failure'] sys.path.append(os.path.abspath('./exts')) -extensions = ['inline', 'nxt', 'subs', 'github'] +extensions = ['inline', 'nxt', 'subs', 'github', 'lunr_search'] smartquotes_action = 'qe' diff --git a/source/exts/lunr_search.py b/source/exts/lunr_search.py new file mode 100644 index 00000000..0febd258 --- /dev/null +++ b/source/exts/lunr_search.py @@ -0,0 +1,124 @@ +""" +Sphinx extension: generate a lunr.js-compatible search index. + +At build time this collects all HTML pages and writes +``search_index.json`` to the output directory. The JSON file is an +array of objects: + + { "id": "", "title": "", "body": "" } + +The ``id`` is the Sphinx ``pagename`` (e.g. ``"configuration/index"``). +The client-side JS converts it to a URL with :func:`pageUrl`. + +Usage (conf.py): + extensions += ['lunr_search'] +""" + +import json +import re + +from pathlib import Path + +from sphinx.application import Sphinx +from sphinx.util import logging as sphinx_logging + +logger = sphinx_logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_TAG_RE = re.compile(r"<[^>]+>") +_SPACE_RE = re.compile(r"\s+") + +# Maximum body characters stored per page (keeps the JSON manageable). +_MAX_BODY = 5_000 + + +def _strip_html(html: str) -> str: + text = _TAG_RE.sub(" ", html) + return _SPACE_RE.sub(" ", text).strip() + + +def _normalize_title(pagename: str, title: str) -> str: + """Normalize and clean up page title, fallback to pagename if needed.""" + title = title.strip() + if not title or title == "/" or title in ("", "<no title>"): + if pagename in ("index", "contents"): + title = "Home" + else: + parts = pagename.split('/') + title = " ".join(p.replace('-', ' ').title() for p in parts if p) + return title + + +# --------------------------------------------------------------------------- +# Extension state +# --------------------------------------------------------------------------- + +class _State: + def __init__(self) -> None: + self.pages: list[dict] = [] + + def reset(self) -> None: + self.pages.clear() + + +_state = _State() + + +# --------------------------------------------------------------------------- +# Event handlers +# --------------------------------------------------------------------------- + +def _on_env_before_read_docs(app, env, docnames): # noqa: unused args + _state.reset() + + +def _on_html_page_context(app, pagename, templatename, context, doctree): + # Skip the Sphinx search page itself and the root index redirect. + if pagename == "search": + return + + raw_title = context.get("title", "") or "" + body = context.get("body", "") or "" + + if not body: + return + + title = _normalize_title(pagename, _strip_html(raw_title)) + text = _strip_html(body)[:_MAX_BODY] + + _state.pages.append({ + "id": pagename, + "title": title, + "body": text, + }) + + +def _on_build_finished(app, exception): + if exception: + return + + out_path = Path(app.outdir) / "search_index.json" + out_path.write_text( + json.dumps(_state.pages, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + logger.info(f"lunr_search: wrote {len(_state.pages)} entries → {out_path}") + + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- + +def setup(app: Sphinx) -> dict: + app.connect("env-before-read-docs", _on_env_before_read_docs) + app.connect("html-page-context", _on_html_page_context) + app.connect("build-finished", _on_build_finished) + + return { + "version": "1.0", + "parallel_read_safe": True, + } + diff --git a/source/theme/layout.html b/source/theme/layout.html index 409a8e93..c9545b49 100644 --- a/source/theme/layout.html +++ b/source/theme/layout.html @@ -68,6 +68,7 @@ + {%- if next %} @@ -96,6 +97,11 @@

v. {{version}}

+
+ +
+
+
{{ toctree(maxdepth = 4) }}
diff --git a/source/theme/search.html b/source/theme/search.html index e69de29b..c6f89b3a 100644 --- a/source/theme/search.html +++ b/source/theme/search.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block body %} +

Search

+

Use the search box in the sidebar to search the documentation.

+{% endblock %} diff --git a/source/theme/static/lunr.min.js b/source/theme/static/lunr.min.js new file mode 100644 index 00000000..cdc94cd3 --- /dev/null +++ b/source/theme/static/lunr.min.js @@ -0,0 +1,6 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ +!function(){var e=function(t){var r=new e.Builder;return r.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),r.searchPipeline.add(e.stemmer),t.call(r,r),r.build()};e.version="2.3.9",e.utils={},e.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),e.utils.asString=function(e){return void 0===e||null===e?"":e.toString()},e.utils.clone=function(e){if(null===e||void 0===e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i0){var c=e.utils.clone(r)||{};c.position=[a,l],c.index=s.length,s.push(new e.Token(i.slice(a,o),c))}a=o+1}}return s},e.tokenizer.separator=/[\s\-]+/,e.Pipeline=function(){this._stack=[]},e.Pipeline.registeredFunctions=Object.create(null),e.Pipeline.registerFunction=function(t,r){r in this.registeredFunctions&&e.utils.warn("Overwriting existing registered function: "+r),t.label=r,e.Pipeline.registeredFunctions[t.label]=t},e.Pipeline.warnIfFunctionNotRegistered=function(t){var r=t.label&&t.label in this.registeredFunctions;r||e.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",t)},e.Pipeline.load=function(t){var r=new e.Pipeline;return t.forEach(function(t){var i=e.Pipeline.registeredFunctions[t];if(!i)throw new Error("Cannot load unregistered function: "+t);r.add(i)}),r},e.Pipeline.prototype.add=function(){var t=Array.prototype.slice.call(arguments);t.forEach(function(t){e.Pipeline.warnIfFunctionNotRegistered(t),this._stack.push(t)},this)},e.Pipeline.prototype.after=function(t,r){e.Pipeline.warnIfFunctionNotRegistered(r);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");i+=1,this._stack.splice(i,0,r)},e.Pipeline.prototype.before=function(t,r){e.Pipeline.warnIfFunctionNotRegistered(r);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");this._stack.splice(i,0,r)},e.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);t!=-1&&this._stack.splice(t,1)},e.Pipeline.prototype.run=function(e){for(var t=this._stack.length,r=0;r1&&(se&&(r=n),s!=e);)i=r-t,n=t+Math.floor(i/2),s=this.elements[2*n];return s==e?2*n:s>e?2*n:sa?l+=2:o==a&&(t+=r[u+1]*i[l+1],u+=2,l+=2);return t},e.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},e.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),t=1,r=0;t0){var o,a=s.str.charAt(0);a in s.node.edges?o=s.node.edges[a]:(o=new e.TokenSet,s.node.edges[a]=o),1==s.str.length&&(o["final"]=!0),n.push({node:o,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(0!=s.editsRemaining){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new e.TokenSet;s.node.edges["*"]=u}if(0==s.str.length&&(u["final"]=!0),n.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&n.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),1==s.str.length&&(s.node["final"]=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new e.TokenSet;s.node.edges["*"]=l}1==s.str.length&&(l["final"]=!0),n.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var c,h=s.str.charAt(0),d=s.str.charAt(1);d in s.node.edges?c=s.node.edges[d]:(c=new e.TokenSet,s.node.edges[d]=c),1==s.str.length&&(c["final"]=!0),n.push({node:c,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return i},e.TokenSet.fromString=function(t){for(var r=new e.TokenSet,i=r,n=0,s=t.length;n=e;t--){var r=this.uncheckedNodes[t],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r["char"]]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}},e.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},e.Index.prototype.search=function(t){return this.query(function(r){var i=new e.QueryParser(t,r);i.parse()})},e.Index.prototype.query=function(t){for(var r=new e.Query(this.fields),i=Object.create(null),n=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},e.Builder.prototype.k1=function(e){this._k1=e},e.Builder.prototype.add=function(t,r){var i=t[this._ref],n=Object.keys(this._fields);this._documents[i]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){var t,r;do t=this.next(),r=t.charCodeAt(0);while(r>47&&r<58);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos1&&(t.backup(),t.emit(e.QueryLexer.TERM)),t.ignore(),t.more())return e.QueryLexer.lexText},e.QueryLexer.lexEditDistance=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.EDIT_DISTANCE),e.QueryLexer.lexText},e.QueryLexer.lexBoost=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.BOOST),e.QueryLexer.lexText},e.QueryLexer.lexEOS=function(t){t.width()>0&&t.emit(e.QueryLexer.TERM)},e.QueryLexer.termSeparator=e.tokenizer.separator,e.QueryLexer.lexText=function(t){for(;;){var r=t.next();if(r==e.QueryLexer.EOS)return e.QueryLexer.lexEOS;if(92!=r.charCodeAt(0)){if(":"==r)return e.QueryLexer.lexField;if("~"==r)return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexEditDistance;if("^"==r)return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexBoost;if("+"==r&&1===t.width())return t.emit(e.QueryLexer.PRESENCE),e.QueryLexer.lexText;if("-"==r&&1===t.width())return t.emit(e.QueryLexer.PRESENCE),e.QueryLexer.lexText;if(r.match(e.QueryLexer.termSeparator))return e.QueryLexer.lexTerm}else t.escapeCharacter()}},e.QueryParser=function(t,r){this.lexer=new e.QueryLexer(t),this.query=r,this.currentClause={},this.lexemeIdx=0},e.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var t=e.QueryParser.parseClause;t;)t=t(this);return this.query},e.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},e.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},e.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},e.QueryParser.parseClause=function(t){var r=t.peekLexeme();if(void 0!=r)switch(r.type){case e.QueryLexer.PRESENCE:return e.QueryParser.parsePresence;case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(i+=" with value '"+r.str+"'"),new e.QueryParseError(i,r.start,r.end)}},e.QueryParser.parsePresence=function(t){var r=t.consumeLexeme();if(void 0!=r){switch(r.str){case"-":t.currentClause.presence=e.Query.presence.PROHIBITED;break;case"+":t.currentClause.presence=e.Query.presence.REQUIRED;break;default:var i="unrecognised presence operator'"+r.str+"'";throw new e.QueryParseError(i,r.start,r.end)}var n=t.peekLexeme();if(void 0==n){var i="expecting term or field, found nothing";throw new e.QueryParseError(i,r.start,r.end)}switch(n.type){case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expecting term or field, found '"+n.type+"'";throw new e.QueryParseError(i,n.start,n.end)}}},e.QueryParser.parseField=function(t){var r=t.consumeLexeme();if(void 0!=r){if(t.query.allFields.indexOf(r.str)==-1){var i=t.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),n="unrecognised field '"+r.str+"', possible fields: "+i;throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.fields=[r.str];var s=t.peekLexeme();if(void 0==s){var n="expecting term, found nothing";throw new e.QueryParseError(n,r.start,r.end)}switch(s.type){case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var n="expecting term, found '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},e.QueryParser.parseTerm=function(t){var r=t.consumeLexeme();if(void 0!=r){t.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(t.currentClause.usePipeline=!1);var i=t.peekLexeme();if(void 0==i)return void t.nextClause();switch(i.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+i.type+"'";throw new e.QueryParseError(n,i.start,i.end)}}},e.QueryParser.parseEditDistance=function(t){var r=t.consumeLexeme();if(void 0!=r){var i=parseInt(r.str,10);if(isNaN(i)){var n="edit distance must be numeric";throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.editDistance=i;var s=t.peekLexeme();if(void 0==s)return void t.nextClause();switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},e.QueryParser.parseBoost=function(t){var r=t.consumeLexeme();if(void 0!=r){var i=parseInt(r.str,10);if(isNaN(i)){var n="boost must be numeric";throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.boost=i;var s=t.peekLexeme();if(void 0==s)return void t.nextClause();switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():e.lunr=t()}(this,function(){return e})}(); diff --git a/source/theme/static/script.js b/source/theme/static/script.js index b578d0da..25d7cdb2 100644 --- a/source/theme/static/script.js +++ b/source/theme/static/script.js @@ -188,6 +188,7 @@ function nxt_dom_ready() { nxt_scroll_init() nxt_tab_init() nxt_hash_change() + nxt_search_init() if (IntersectionObserver) { nxt_nav_init() @@ -233,3 +234,209 @@ if (document.readyState === 'loading') { } else { nxt_dom_ready() } + + +/* Lunr.js full-text search */ + +let _nxt_search_index = null +let _nxt_search_docs = null +let _nxt_search_promise = null + + +function nxt_search_index_url() { + const depth = window.location.pathname + .replace(/\/$/, '') + .split('/') + .length - 1 + const prefix = depth > 0 ? '../'.repeat(depth) : './' + return prefix + 'search_index.json' +} + + +function nxt_search_page_url(pagename) { + // Convert a Sphinx pagename to a site-relative URL the same way + // DirectoryHTMLBuilder does: + // "index" → "/" + // "configuration/index"→ "/configuration/" + // "installation" → "/installation/" + if (pagename === 'index' || pagename === 'contents') return '/' + const clean = pagename.replace(/\/index$/, '') + return '/' + clean + '/' +} + + +function nxt_search_load_index() { + if (_nxt_search_promise) return _nxt_search_promise + _nxt_search_promise = fetch(nxt_search_index_url()) + .then(r => r.json()) + .then(pages => { + _nxt_search_docs = {} + pages.forEach(p => { _nxt_search_docs[p.id] = p }) + + // Title gets 15x boost + _nxt_search_index = lunr(function() { + this.ref('id') + this.field('title', { boost: 15 }) + this.field('body', { boost: 1 }) + pages.forEach(function(p) { this.add(p) }, this) + }) + }) + return _nxt_search_promise +} + + +function nxt_search_highlight(text, query) { + const words = query.trim().split(/\s+/).filter(Boolean) + let snippet = text.slice(0, 160) + words.forEach(w => { + const re = new RegExp('(' + w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi') + snippet = snippet.replace(re, '$1') + }) + return snippet + (text.length > 160 ? '…' : '') +} + + +function nxt_search_render_results(results, query, container) { + container.innerHTML = '' + if (!results.length) { + container.innerHTML = '
No results found.
' + container.classList.add('nxt_search_open') + return + } + results.slice(0, 12).forEach(r => { + const doc = _nxt_search_docs[r.ref] + const url = nxt_search_page_url(r.ref) + const item = document.createElement('div') + item.className = 'nxt_search_item' + + const a = document.createElement('a') + a.href = url + + const title = document.createElement('div') + title.className = 'nxt_search_title' + title.textContent = doc.title + + const snippet = document.createElement('div') + snippet.className = 'nxt_search_snippet' + snippet.innerHTML = nxt_search_highlight(doc.body, query) + + a.appendChild(title) + a.appendChild(snippet) + item.appendChild(a) + container.appendChild(item) + }) + container.classList.add('nxt_search_open') +} + + +function nxt_search_get_suggestions() { + const suggestions = new Set() + Object.values(_nxt_search_docs).forEach(doc => { + suggestions.add(doc.title) + }) + return Array.from(suggestions).sort().slice(0, 5) +} + + +function nxt_search_render_suggestions(container) { + container.innerHTML = '' + const suggestions = nxt_search_get_suggestions() + if (!suggestions.length) return + + const ul = document.createElement('ul') + ul.className = 'nxt_search_suggestion_list' + suggestions.forEach(suggestion => { + const li = document.createElement('li') + li.className = 'nxt_search_suggestion_item' + + const a = document.createElement('a') + a.href = 'javascript:void(0)' + a.textContent = suggestion + + a.addEventListener('click', e => { + e.preventDefault() + document.getElementById('nxt_search_input').value = suggestion + document.getElementById('nxt_search_input').dispatchEvent(new Event('input')) + }) + + li.appendChild(a) + ul.appendChild(li) + }) + container.appendChild(ul) +} + + +function nxt_search_init() { + const input = document.getElementById('nxt_search_input') + const container = document.getElementById('nxt_search_results') + const suggestedContainer = document.getElementById('nxt_search_suggestions') + if (!input || !container) return + + let timer = null + + document.addEventListener('keydown', e => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + input.focus() + input.select() + } + }) + + input.addEventListener('focus', () => { + nxt_search_load_index().then(() => { + if (!input.value.trim()) { + nxt_search_render_suggestions(suggestedContainer) + suggestedContainer.classList.add('nxt_search_open') + } + }) + }) + + input.addEventListener('input', () => { + clearTimeout(timer) + const q = input.value.trim() + + if (!q) { + container.classList.remove('nxt_search_open') + container.innerHTML = '' + nxt_search_load_index().then(() => { + nxt_search_render_suggestions(suggestedContainer) + suggestedContainer.classList.add('nxt_search_open') + }) + return + } + + suggestedContainer.classList.remove('nxt_search_open') + + timer = setTimeout(() => { + nxt_search_load_index().then(() => { + let results = _nxt_search_index.search(q + '~1') // fuzzy + if (!results.length) results = _nxt_search_index.search(q) // exact fallback + nxt_search_render_results(results, q, container) + }) + }, 200) + }) + + input.addEventListener('keydown', e => { + if (e.key === 'Enter') { + const first = container.querySelector('.nxt_search_item a') + if (first) { window.location.href = first.href } + } + if (e.key === 'Escape') { + container.classList.remove('nxt_search_open') + suggestedContainer.classList.remove('nxt_search_open') + input.value = '' + } + }) + + document.addEventListener('click', e => { + if (!input.contains(e.target) && !container.contains(e.target) && !suggestedContainer.contains(e.target)) { + container.classList.remove('nxt_search_open') + suggestedContainer.classList.remove('nxt_search_open') + } + }) + + if ('requestIdleCallback' in window) { + requestIdleCallback(nxt_search_load_index) + } +} + diff --git a/source/theme/static/style.css b/source/theme/static/style.css index 209c1b45..d0843222 100644 --- a/source/theme/static/style.css +++ b/source/theme/static/style.css @@ -851,6 +851,147 @@ iframe { } } +/* Search */ + +#nxt_search_wrap { + position: relative; + margin: .5em .4em .6em .4em; +} +#nxt_search_input { + box-sizing: border-box; + width: 100%; + padding: .35em .6em; + border: .1em solid #ccc; + border-radius: .3em; + font: inherit; + font-size: .9em; + outline: none; + background: white; + color: #333; +} +#nxt_search_input:focus { + border-color: #00974d; +} +#nxt_search_results { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 100; + background: white; + border: .1em solid #ccc; + border-top: none; + border-radius: 0 0 .3em .3em; + max-height: 60vh; + overflow-y: auto; + box-shadow: 0 .4em .8em rgba(0,0,0,.15); +} +#nxt_search_results.nxt_search_open { + display: block; +} +#nxt_search_suggestions { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 100; + background: white; + border: .1em solid #ccc; + border-top: none; + border-radius: 0 0 .3em .3em; + box-shadow: 0 .4em .8em rgba(0,0,0,.15); +} +#nxt_search_suggestions.nxt_search_open { + display: block; +} +.nxt_search_suggestion_list { + list-style: none; + margin: 0; + padding: 0; +} +.nxt_search_suggestion_item { + padding: .5em .7em; + border-bottom: .05em solid #eee; +} +.nxt_search_suggestion_item:last-child { + border-bottom: none; +} +.nxt_search_suggestion_item a { + display: block; + text-decoration: none; + color: #666; + font-size: .85em; +} +.nxt_search_suggestion_item:hover a { + color: #00974d; +} +.nxt_search_item { + padding: .5em .7em; + border-bottom: .05em solid #eee; + cursor: pointer; +} +.nxt_search_item:last-child { + border-bottom: none; +} +.nxt_search_item a { + display: block; + text-decoration: none; + color: inherit; +} +.nxt_search_item:hover, +.nxt_search_item:focus-within { + background: #f0faf5; +} +.nxt_search_title { + font-size: .9em; + font-weight: bold; + color: #00974d; +} +.nxt_search_snippet { + font-size: .8em; + color: #666; + margin-top: .15em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.nxt_search_none { + padding: .6em .7em; + font-size: .85em; + color: #999; +} + +@media (prefers-color-scheme: dark) { + #nxt_search_input { + background: #273549; + color: #cbd5e1; + border-color: #4a5568; + } + #nxt_search_results { + background: #1e293b; + border-color: #4a5568; + } + #nxt_search_suggestions { + background: #1e293b; + border-color: #4a5568; + } + .nxt_search_item:hover, + .nxt_search_item:focus-within { + background: #273549; + } + .nxt_search_suggestion_item a { + color: #94a3b8; + } + .nxt_search_suggestion_item:hover a { + color: #00974d; + } + .nxt_search_snippet { + color: #94a3b8; + } +} + blockquote { margin: 1em 0; border-color: #1e293b;