Add base files

This commit is contained in:
clydebarrow 2025-06-12 12:32:15 +10:00
parent 77ab90f719
commit 349c57f9ce
56 changed files with 4332 additions and 83 deletions

31
.gitignore vendored
View File

@ -1,18 +1,13 @@
_build
.DS_Store
.python-version
__pycache__/
*.py[cod]
*$py.class
venv
.vscode
*.DS_Store
/.idea/
_pagefind/
# Vim
*.swp
public/
esphome-docs
.hugo_build.lock
.idea
/node_modules/
/package.json
/package-lock.json
/pagefind/
/data/repo.yaml
/data/anchors.json
/data/automations
my-secrets
resources/_gen

View File

@ -1,66 +1,45 @@
ESPHOME_PATH = ../esphome
ESPHOME_REF = 2025.5.2
PAGEFIND_VERSION=1.1.1
PAGEFIND=pagefind
NET_PAGEFIND=../pagefindbin/pagefind
.PHONY: html clean live-html automations check-links anchors production convert-from-rst directories
.PHONY: pagefind build-html html html-strict cleanhtml deploy help live-html live-pagefind Makefile netlify netlify-dependencies svg2png copy-svg2png minify
export HUGO_PARAMS_COMMIT_HASH=$(shell git rev-parse --short HEAD)
export HUGO_PARAMS_COMMIT_TITLE=$(shell git log -1 --pretty=%s)
export HUGO_PARAMS_COMMIT_DATE=$(shell git log -1 --date=format-local:'%Y-%m-%d %H:%M:%S UTC' --pretty=%cd)
export HUGO_PARAMS_BRANCH=$(shell git branch --show-current)
html: pagefind
sphinx-build -M html . _build -j auto -n $(O) -Dhtml_extra_path=_redirects,_pagefind
production: repo-data anchors
hugo --minify
npx pagefind
hugo --minify
pagefind:
sphinx-build -M html . _build -j auto -n $(O)
mkdir -p _pagefind/pagefind
${PAGEFIND}
directories:
mkdir -p data public pagefind content static
npx pagefind -s pagefind-bootstrap
live-html: pagefind
sphinx-autobuild . _build -j auto -n $(O) --host 0.0.0.0 -Dhtml_extra_path=_redirects,_pagefind
check-links: repo-data anchors
hugo --environment production
html-strict:
sphinx-build -M html . _build -W -j auto -n $(O)
anchors: directories
hugo --environment anchors
python3 tools/md_anchors.py
minify:
minify _static/webserver-v1.js > _static/webserver-v1.min.js
minify _static/webserver-v1.css > _static/webserver-v1.min.css
repo-data: directories
mkdir -p data/automations
echo "url: `git config --get remote.origin.url`" > data/repo.yaml
echo "branch: `git branch --show-current`" >> data/repo.yaml
curl -s -S https://data.esphome.io/release/automations.json | tools/collate_automations.sh > data/automations/current.json
curl -s -S https://data.esphome.io/beta/automations.json | tools/collate_automations.sh > data/automations/beta.json
curl -s -S https://data.esphome.io/dev/automations.json | tools/collate_automations.sh > data/automations/next.json
cleanhtml:
rm -rf "_build/html/*"
svg2png:
python3 svg2png.py
help:
sphinx-build -M help . _build $(O)
net-html:
sed -i 's@{{API_DOCS_URL}}@'"${API_DOCS_URL}"'@' _redirects
sphinx-build -M html . _build -j auto -n $(O)
mkdir -p _pagefind/pagefind
${NET_PAGEFIND}
sphinx-build -M html . _build -j auto -n $(O) -Dhtml_extra_path=_redirects,_pagefind
pagefind-binary:
mkdir -p ../pagefindbin
curl -o pagefind.tar.gz https://github.com/CloudCannon/pagefind/releases/download/v$(PAGEFIND_VERSION)/pagefind-v$(PAGEFIND_VERSION)-x86_64-unknown-linux-musl.tar.gz -L
tar xzf pagefind.tar.gz
rm pagefind.tar.gz
mv pagefind ${NET_PAGEFIND}
copy-svg2png:
cp svg2png/*.png _build/html/_images/
netlify: pagefind-binary net-html copy-svg2png
lint: html-strict
python3 lint.py
live-html: repo-data anchors
npx pagefind
env | grep HUGO
hugo server --bind 0.0.0.0
clean:
rm -rf _pagefind/
sphinx-build -M clean . _build $(O)
rm -rf "public/*"
rm -rf "pagefind/*"
rm -rf data/automations/
rm -rf data/repo.yaml
hugo mod clean
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
sphinx-build -M $@ . _build $(O)
convert-from-rst:
python3 tools/convert_rst_to_md.py ./esphome-docs .

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
</head>
<body>
<div class="page-container">
<main data-pagefind-body>
<h1>Dummy page</h1>
<p>
This file exists just to feed Pagefind before the site is built, in order to populate the pagefind directory
</p>
</main>
</div>
</body>
</html>

View File

@ -1,10 +1,8 @@
site: _build/html
output_path: _pagefind/pagefind
site: public
output_path: pagefind
exclude_selectors:
- "a.headerlink"
- ".toctree-wrapper"
- ".sphinxsidebar"
- ".breadcrumbs"
- "pre"
glob: "{components,cookbook,guides,projects,web-api}/**/*.html"
root_selector: div[role=main]

View File

@ -1,5 +0,0 @@
sphinx==7.1.2
sphinx-autobuild==2021.3.14
sphinx-tabs==3.4.7
sphinx-toolbox==3.8.0
sphinx-copybutton==0.5.2

View File

@ -0,0 +1,605 @@
/* Background */ .bg { color:#272822;background-color:#fafafa; }
/* PreWrapper */ .chroma { color:#272822;background-color:#fafafa; }
/* Other */ .chroma .x { }
/* Error */ .chroma .err { color:#960050;background-color:#1e0010 }
/* CodeLine */ .chroma .cl { }
/* LineLink */ .chroma .lnlinks { outline:none;text-decoration:none;color:inherit }
/* LineTableTD */ .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; }
/* LineTable */ .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; }
/* LineHighlight */ .chroma .hl { background-color:#e1e1e1 }
/* LineNumbersTable */ .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f }
/* LineNumbers */ .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f }
/* Line */ .chroma .line { display:flex; }
/* Keyword */ .chroma .k { color:#00a8c8 }
/* KeywordConstant */ .chroma .kc { color:#00a8c8 }
/* KeywordDeclaration */ .chroma .kd { color:#00a8c8 }
/* KeywordNamespace */ .chroma .kn { color:#f92672 }
/* KeywordPseudo */ .chroma .kp { color:#00a8c8 }
/* KeywordReserved */ .chroma .kr { color:#00a8c8 }
/* KeywordType */ .chroma .kt { color:#00a8c8 }
/* Name */ .chroma .n { color:#111 }
/* NameAttribute */ .chroma .na { color:#75af00 }
/* NameBuiltin */ .chroma .nb { color:#111 }
/* NameBuiltinPseudo */ .chroma .bp { color:#111 }
/* NameClass */ .chroma .nc { color:#75af00 }
/* NameConstant */ .chroma .no { color:#00a8c8 }
/* NameDecorator */ .chroma .nd { color:#75af00 }
/* NameEntity */ .chroma .ni { color:#111 }
/* NameException */ .chroma .ne { color:#75af00 }
/* NameFunction */ .chroma .nf { color:#75af00 }
/* NameFunctionMagic */ .chroma .fm { color:#111 }
/* NameLabel */ .chroma .nl { color:#111 }
/* NameNamespace */ .chroma .nn { color:#111 }
/* NameOther */ .chroma .nx { color:#75af00 }
/* NameProperty */ .chroma .py { color:#111 }
/* NameTag */ .chroma .nt { color:#f92672 }
/* NameVariable */ .chroma .nv { color:#111 }
/* NameVariableClass */ .chroma .vc { color:#111 }
/* NameVariableGlobal */ .chroma .vg { color:#111 }
/* NameVariableInstance */ .chroma .vi { color:#111 }
/* NameVariableMagic */ .chroma .vm { color:#111 }
/* Literal */ .chroma .l { color:#ae81ff }
/* LiteralDate */ .chroma .ld { color:#d88200 }
/* LiteralString */ .chroma .s { color:#d88200 }
/* LiteralStringAffix */ .chroma .sa { color:#d88200 }
/* LiteralStringBacktick */ .chroma .sb { color:#d88200 }
/* LiteralStringChar */ .chroma .sc { color:#d88200 }
/* LiteralStringDelimiter */ .chroma .dl { color:#d88200 }
/* LiteralStringDoc */ .chroma .sd { color:#d88200 }
/* LiteralStringDouble */ .chroma .s2 { color:#d88200 }
/* LiteralStringEscape */ .chroma .se { color:#8045ff }
/* LiteralStringHeredoc */ .chroma .sh { color:#d88200 }
/* LiteralStringInterpol */ .chroma .si { color:#d88200 }
/* LiteralStringOther */ .chroma .sx { color:#d88200 }
/* LiteralStringRegex */ .chroma .sr { color:#d88200 }
/* LiteralStringSingle */ .chroma .s1 { color:#d88200 }
/* LiteralStringSymbol */ .chroma .ss { color:#d88200 }
/* LiteralNumber */ .chroma .m { color:#ae81ff }
/* LiteralNumberBin */ .chroma .mb { color:#ae81ff }
/* LiteralNumberFloat */ .chroma .mf { color:#ae81ff }
/* LiteralNumberHex */ .chroma .mh { color:#ae81ff }
/* LiteralNumberInteger */ .chroma .mi { color:#ae81ff }
/* LiteralNumberIntegerLong */ .chroma .il { color:#ae81ff }
/* LiteralNumberOct */ .chroma .mo { color:#ae81ff }
/* Operator */ .chroma .o { color:#f92672 }
/* OperatorWord */ .chroma .ow { color:#f92672 }
/* Punctuation */ .chroma .p { color:#111 }
/* Comment */ .chroma .c { color:#75715e }
/* CommentHashbang */ .chroma .ch { color:#75715e }
/* CommentMultiline */ .chroma .cm { color:#75715e }
/* CommentSingle */ .chroma .c1 { color:#75715e }
/* CommentSpecial */ .chroma .cs { color:#75715e }
/* CommentPreproc */ .chroma .cp { color:#75715e }
/* CommentPreprocFile */ .chroma .cpf { color:#75715e }
/* Generic */ .chroma .g { }
/* GenericDeleted */ .chroma .gd { }
/* GenericEmph */ .chroma .ge { font-style:italic }
/* GenericError */ .chroma .gr { }
/* GenericHeading */ .chroma .gh { }
/* GenericInserted */ .chroma .gi { }
/* GenericOutput */ .chroma .go { }
/* GenericPrompt */ .chroma .gp { }
/* GenericStrong */ .chroma .gs { font-weight:bold }
/* GenericSubheading */ .chroma .gu { }
/* GenericTraceback */ .chroma .gt { }
/* GenericUnderline */ .chroma .gl { }
/* TextWhitespace */ .chroma .w { }
[data-theme="dark"] {
/* Background */
.bg {
color: #f8f8f2;
background-color: #272822;
}
/* PreWrapper */
.chroma {
color: #f8f8f2;
background-color: #272822;
}
/* Other */
.chroma .x {
}
/* Error */
.chroma .err {
color: #960050;
background-color: #1e0010
}
/* CodeLine */
.chroma .cl {
}
/* LineLink */
.chroma .lnlinks {
outline: none;
text-decoration: none;
color: inherit
}
/* LineTableTD */
.chroma .lntd {
vertical-align: top;
padding: 0;
margin: 0;
border: 0;
}
/* LineTable */
.chroma .lntable {
border-spacing: 0;
padding: 0;
margin: 0;
border: 0;
}
/* LineHighlight */
.chroma .hl {
background-color: #3c3d38
}
/* LineNumbersTable */
.chroma .lnt {
white-space: pre;
-webkit-user-select: none;
user-select: none;
margin-right: 0.4em;
padding: 0 0.4em 0 0.4em;
color: #7f7f7f
}
/* LineNumbers */
.chroma .ln {
white-space: pre;
-webkit-user-select: none;
user-select: none;
margin-right: 0.4em;
padding: 0 0.4em 0 0.4em;
color: #7f7f7f
}
/* Line */
.chroma .line {
display: flex;
}
/* Keyword */
.chroma .k {
color: #66d9ef
}
/* KeywordConstant */
.chroma .kc {
color: #66d9ef
}
/* KeywordDeclaration */
.chroma .kd {
color: #66d9ef
}
/* KeywordNamespace */
.chroma .kn {
color: #f92672
}
/* KeywordPseudo */
.chroma .kp {
color: #66d9ef
}
/* KeywordReserved */
.chroma .kr {
color: #66d9ef
}
/* KeywordType */
.chroma .kt {
color: #66d9ef
}
/* Name */
.chroma .n {
}
/* NameAttribute */
.chroma .na {
color: #a6e22e
}
/* NameBuiltin */
.chroma .nb {
}
/* NameBuiltinPseudo */
.chroma .bp {
}
/* NameClass */
.chroma .nc {
color: #a6e22e
}
/* NameConstant */
.chroma .no {
color: #66d9ef
}
/* NameDecorator */
.chroma .nd {
color: #a6e22e
}
/* NameEntity */
.chroma .ni {
}
/* NameException */
.chroma .ne {
color: #a6e22e
}
/* NameFunction */
.chroma .nf {
color: #a6e22e
}
/* NameFunctionMagic */
.chroma .fm {
}
/* NameLabel */
.chroma .nl {
}
/* NameNamespace */
.chroma .nn {
}
/* NameOther */
.chroma .nx {
color: #a6e22e
}
/* NameProperty */
.chroma .py {
}
/* NameTag */
.chroma .nt {
color: #f92672
}
/* NameVariable */
.chroma .nv {
}
/* NameVariableClass */
.chroma .vc {
}
/* NameVariableGlobal */
.chroma .vg {
}
/* NameVariableInstance */
.chroma .vi {
}
/* NameVariableMagic */
.chroma .vm {
}
/* Literal */
.chroma .l {
color: #ae81ff
}
/* LiteralDate */
.chroma .ld {
color: #e6db74
}
/* LiteralString */
.chroma .s {
color: #e6db74
}
/* LiteralStringAffix */
.chroma .sa {
color: #e6db74
}
/* LiteralStringBacktick */
.chroma .sb {
color: #e6db74
}
/* LiteralStringChar */
.chroma .sc {
color: #e6db74
}
/* LiteralStringDelimiter */
.chroma .dl {
color: #e6db74
}
/* LiteralStringDoc */
.chroma .sd {
color: #e6db74
}
/* LiteralStringDouble */
.chroma .s2 {
color: #e6db74
}
/* LiteralStringEscape */
.chroma .se {
color: #ae81ff
}
/* LiteralStringHeredoc */
.chroma .sh {
color: #e6db74
}
/* LiteralStringInterpol */
.chroma .si {
color: #e6db74
}
/* LiteralStringOther */
.chroma .sx {
color: #e6db74
}
/* LiteralStringRegex */
.chroma .sr {
color: #e6db74
}
/* LiteralStringSingle */
.chroma .s1 {
color: #e6db74
}
/* LiteralStringSymbol */
.chroma .ss {
color: #e6db74
}
/* LiteralNumber */
.chroma .m {
color: #ae81ff
}
/* LiteralNumberBin */
.chroma .mb {
color: #ae81ff
}
/* LiteralNumberFloat */
.chroma .mf {
color: #ae81ff
}
/* LiteralNumberHex */
.chroma .mh {
color: #ae81ff
}
/* LiteralNumberInteger */
.chroma .mi {
color: #ae81ff
}
/* LiteralNumberIntegerLong */
.chroma .il {
color: #ae81ff
}
/* LiteralNumberOct */
.chroma .mo {
color: #ae81ff
}
/* Operator */
.chroma .o {
color: #f92672
}
/* OperatorWord */
.chroma .ow {
color: #f92672
}
/* Punctuation */
.chroma .p {
color: #f8f8f2
}
/* Comment */
.chroma .c {
color: #75715e
}
/* CommentHashbang */
.chroma .ch {
color: #75715e
}
/* CommentMultiline */
.chroma .cm {
color: #75715e
}
/* CommentSingle */
.chroma .c1 {
color: #75715e
}
/* CommentSpecial */
.chroma .cs {
color: #75715e
}
/* CommentPreproc */
.chroma .cp {
color: #75715e
}
/* CommentPreprocFile */
.chroma .cpf {
color: #75715e
}
/* Generic */
.chroma .g {
}
/* GenericDeleted */
.chroma .gd {
color: #f92672
}
/* GenericEmph */
.chroma .ge {
font-style: italic
}
/* GenericError */
.chroma .gr {
}
/* GenericHeading */
.chroma .gh {
}
/* GenericInserted */
.chroma .gi {
color: #a6e22e
}
/* GenericOutput */
.chroma .go {
}
/* GenericPrompt */
.chroma .gp {
}
/* GenericStrong */
.chroma .gs {
font-weight: bold
}
/* GenericSubheading */
.chroma .gu {
color: #75715e
}
/* GenericTraceback */
.chroma .gt {
}
/* GenericUnderline */
.chroma .gl {
}
/* TextWhitespace */
.chroma .w {
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
/* Table color variables */
:root {
--table-border-color: #e0e0e0;
--table-header-bg: #00979d;
--table-header-text: #fff;
--table-code-bg: rgba(0, 0, 0, 0.05);
--table-row-alt-bg: #f8f8f8;
--table-hover-bg: rgba(0, 151, 157, 0.05);
}
[data-theme="dark"] {
--table-border-color: #333b3d;
--table-header-bg: #1abfc6;
--table-header-text: #111;
--table-code-bg: rgba(255,255,255,0.08);
--table-row-alt-bg: #232728;
--table-hover-bg: rgba(26, 191, 198, 0.08);
}
/* Table styling for ESPHome documentation */
table {
max-width: 100%;
width: auto;
border-collapse: collapse;
margin: 1.5em 0;
font-size: 0.9em;
border: 1px solid var(--table-border-color);
table-layout: auto;
}
/* Basic cell styling */
table td,
table th {
padding: 10px 15px;
border: 1px solid var(--table-border-color);
vertical-align: top;
}
/* All table headers should have this styling */
table th {
background-color: var(--table-header-bg);
color: var(--table-header-text);
font-weight: bold;
}
/* For Markdown tables where the first row contains th elements */
table tr:first-child th {
background-color: var(--table-header-bg);
color: var(--table-header-text);
font-weight: bold;
}
/* Override code styling within table headers */
table th code,
table tr:first-child th code {
background-color: transparent;
color: var(--table-header-text);
padding: 0;
font-weight: bold;
border-radius: 0;
}
/* Style for code in table cells */
table td code {
background-color: var(--table-code-bg);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
/* Alternating row colors for better readability */
table tr:nth-child(even) {
background-color: var(--table-row-alt-bg);
}
/* But header rows should always have the header background */
table tr:nth-child(even) th {
background-color: var(--table-header-bg);
}
/* Hover effect */
table tr:hover td {
background-color: var(--table-hover-bg);
}
/* Style for centered tables */
table th[align="center"],
table td[align="center"] {
text-align: center;
}
/* Style for right-aligned tables */
table th[align="right"],
table td[align="right"] {
text-align: right;
}
/* Responsive tables for smaller screens */
@media (max-width: 600px) {
table {
display: block;
overflow-x: auto;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,217 @@
function trapScroll(el) {
el.addEventListener('wheel', (e) => {
const scrollTop = el.scrollTop;
const scrollHeight = el.scrollHeight;
const offsetHeight = el.offsetHeight;
const delta = e.deltaY;
const atTop = scrollTop === 0;
const atBottom = scrollTop + offsetHeight >= scrollHeight;
if ((atTop && delta < 0) || (atBottom && delta > 0)) {
e.preventDefault();
}
}, {passive: false});
}
function trapTouchScroll(el) {
let startY = 0;
el.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
el.addEventListener('touchmove', (e) => {
const scrollTop = el.scrollTop;
const scrollHeight = el.scrollHeight;
const offsetHeight = el.offsetHeight;
const currentY = e.touches[0].clientY;
const deltaY = currentY - startY;
const atTop = scrollTop === 0;
const atBottom = scrollTop + offsetHeight >= scrollHeight;
if ((atTop && deltaY > 0) || (atBottom && deltaY < 0)) {
e.preventDefault();
}
}, {passive: false});
}
document.addEventListener('DOMContentLoaded', function () {
const scrollers = document.querySelectorAll('.scroll-trap');
for (let i = 0; i !== scrollers.length; i++) {
trapScroll(scrollers[i]);
trapTouchScroll(scrollers[i]);
}
document.querySelectorAll('.copy-link').forEach(button => {
button.addEventListener('click', async () => {
const anchor = button.getAttribute('data-anchor');
const url = `${window.location.origin}${window.location.pathname}#${anchor}`;
await navigator.clipboard.writeText(url)
// Remove the class if its already there (to restart the animation)
button.classList.remove('spin-once');
// Trigger reflow to "restart" the animation
void button.offsetWidth;
// Add the class to trigger the spin
button.classList.add('spin-once');
});
});
// Copy button functionality
const copyButtons = document.querySelectorAll('.copy-button');
copyButtons.forEach(button => {
button.addEventListener('click', async () => {
const codeBlock = button.closest('.code-block');
const yamlContent = codeBlock.querySelector('.codeblock-content');
const code = yamlContent.textContent;
try {
await navigator.clipboard.writeText(code);
const feedback = button.querySelector(".copy-feedback");
if (feedback) {
feedback.textContent = "Copied!";
}
button.classList.add('copied');
setTimeout(() => {
button.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
});
});
const scrollThreshold = 5; // Minimum scroll amount before triggering hide/show
const navContainer = document.getElementById('nav-container');
let scrollDelta = 0; // Track cumulative scroll amount
let lastScrollTop = 0;
function scroll_bar(newDelta) {
let navHeight = navContainer.offsetHeight;
// Remove the transition class when scrolling down for direct tracking
navContainer.classList.remove('nav-scrolling-up');
// Increase the scroll delta by the amount scrolled - start immediately from top
scrollDelta += newDelta;
// Cap the scroll delta at the nav height
scrollDelta = Math.min(scrollDelta, navHeight);
// Apply the transform
navContainer.style.transform = `translateY(-${scrollDelta}px)`;
// If fully hidden, add the nav-hidden class
if (scrollDelta >= navHeight) {
navContainer.classList.add('nav-hidden');
}
}
// Header scroll behavior
let ticking = false; // Flag to prevent multiple rAF calls
let tocScroll = false;
let scrollEndTimeout = null;
function handleScroll() {
if (tocScroll) {
clearTimeout(scrollEndTimeout);
scrollEndTimeout = setTimeout( () => { tocScroll = false; }, 100);
ticking = false;
return;
}
const currentScrollTop = window.scrollY || document.documentElement.scrollTop;
// Check if we've scrolled more than the threshold
if (Math.abs(lastScrollTop - currentScrollTop) <= scrollThreshold) {
ticking = false;
return;
}
// Scrolling down - directly track the scroll position
if (currentScrollTop > lastScrollTop) {
scroll_bar(currentScrollTop - lastScrollTop);
}
// Scrolling up - smooth transition back
else if (currentScrollTop < lastScrollTop) {
// Reset the scroll delta
scrollDelta = 0;
// Add transition class for smooth appearance
navContainer.classList.add('nav-scrolling-up');
navContainer.classList.remove('nav-hidden');
navContainer.style.transform = 'translateY(0)';
}
lastScrollTop = currentScrollTop;
ticking = false;
}
// Use requestAnimationFrame for better performance
window.addEventListener('scroll', function () {
if (!ticking) {
requestAnimationFrame(handleScroll);
ticking = true;
}
});
const buildInfoButton = document.getElementById('build-info-button');
const buildInfoPopup = document.getElementById('build-info-popup');
const buildInfoClose = document.querySelector('.build-info-close');
if (buildInfoButton && buildInfoPopup) {
buildInfoButton.addEventListener('click', function() {
buildInfoPopup.style.display = 'block';
});
buildInfoClose.addEventListener('click', function() {
buildInfoPopup.style.display = 'none';
});
window.addEventListener('click', function(event) {
if (event.target === buildInfoPopup) {
buildInfoPopup.style.display = 'none';
}
});
}
// Table of Contents highlighting
const tocLinks = document.querySelectorAll('.toc-entry');
if (tocLinks.length > 0) {
// Get all headings that correspond to TOC entries
const headings = Array.from(tocLinks).map(link => {
const id = link.getAttribute('href').substring(1);
return document.getElementById(id);
}).filter(Boolean);
// Add smooth scrolling to TOC links
tocLinks.forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault();
closeTOC();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
tocScroll = true;
window.scrollTo({
top: targetElement.offsetTop - 80, // Offset for fixed header
behavior: 'smooth'
});
scroll_bar(navContainer.offsetHeight);
// Update URL hash without jumping
history.pushState(null, null, `#${targetId}`);
}
});
});
}
});

View File

@ -0,0 +1,173 @@
const bodystyle = window.getComputedStyle(document.body);
const mobileWidthStop = parseInt(bodystyle.getPropertyValue('--mobile-width-stop'));
const isMobile = (window.innerWidth <= mobileWidthStop);
function closeMenu() {
const hamburger = document.querySelector('.hamburger-button');
const navLinks = document.querySelector('.nav-links');
navLinks.classList.remove('active');
hamburger.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
}
function openMenu() {
const hamburger = document.querySelector('.hamburger-button');
const navLinks = document.querySelector('.nav-links');
navLinks.classList.add('active');
hamburger.classList.add('active');
hamburger.setAttribute('aria-expanded', 'true');
}
function openTOC() {
const tocToggle = document.getElementById('toc-toggle');
if (!isMobile || !tocToggle) return;
const tocPanel = document.getElementsByClassName('sidebar-mobile')[0];
const overlay = document.getElementById('overlay');
tocToggle.classList.add('open');
tocPanel.classList.add('open');
overlay.classList.add('show');
}
function closeTOC() {
const tocToggle = document.getElementById('toc-toggle');
if (!isMobile || !tocToggle) return;
const tocPanel = document.getElementsByClassName('sidebar-mobile')[0];
const overlay = document.getElementById('overlay');
tocToggle.classList.remove('open');
tocPanel.classList.remove('open');
overlay.classList.remove('show');
}
// Add keyboard support for dropdown menus
document.addEventListener('DOMContentLoaded', function() {
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
document.querySelector('.theme-toggle').setAttribute('aria-label', `Toggle ${theme === 'dark' ? 'light' : 'dark'} mode`);
closeMenu();
}
// Theme toggle functionality
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
setTheme(currentTheme === 'dark' ? 'light' : 'dark');
});
const tocToggle = document.getElementById('toc-toggle');
const overlay = document.getElementById('overlay');
if (tocToggle)
tocToggle.addEventListener('click', event => {
if (tocToggle.classList.contains("open"))
closeTOC();
else
openTOC();
});
if (overlay)
overlay.addEventListener('click', closeTOC);
const sidebarMobile = document.querySelectorAll('.sidebar-mobile');
sidebarMobile.forEach(sidebar => {
sidebar.addEventListener("click", closeTOC);
})
const dropdownButtons = document.querySelectorAll('.dropbtn button');
dropdownButtons.forEach(button => {
// Handle Enter and Space key presses
button.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleDropdown(this);
}
});
// Handle click events
button.addEventListener('click', function(e) {
if (!isMobile) return;
e.preventDefault();
// Close others
dropdownButtons.forEach(function(otherBtn) {
if (otherBtn !== button) {
otherBtn.setAttribute('aria-expanded', 'false');
if (otherBtn.nextElementSibling) {
otherBtn.nextElementSibling.style.display = 'none';
}
}
});
// Toggle this one
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', expanded ? "false" : "true");
if (button.nextElementSibling) {
button.nextElementSibling.style.display = expanded ? 'none' : 'block';
}
});
});
// Close dropdowns when Escape key is pressed
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
closeAllDropdowns();
closeTOC();
}
});
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.matches('.dropbtn')) {
closeAllDropdowns();
}
});
// Toggle dropdown function
function toggleDropdown(button) {
if (window.innerWidth > mobileWidthStop) return;
const isExpanded = button.getAttribute('aria-expanded') === 'true';
closeAllDropdowns();
if (!isExpanded) {
button.setAttribute('aria-expanded', 'true');
const dropdownContent = button.nextElementSibling;
dropdownContent.style.display = 'block';
}
}
// Close all dropdowns
function closeAllDropdowns() {
if (window.innerWidth > mobileWidthStop) return;
dropdownButtons.forEach(btn => {
btn.setAttribute('aria-expanded', 'false');
const dropdownContent = btn.nextElementSibling;
if (dropdownContent)
dropdownContent.style.display = 'none';
});
}
const hamburger = document.querySelector('.hamburger-button');
const navLinks = document.querySelector('.nav-links');
if (!hamburger || !navLinks) return;
hamburger.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
hamburger.classList.toggle('active');
navLinks.classList.toggle('active');
const expanded = hamburger.getAttribute('aria-expanded') === 'true';
hamburger.setAttribute('aria-expanded', expanded ? "false" : "true");
closeTOC();
});
// Close menu on outside click (mobile only)
document.addEventListener('click', function(e) {
if (window.innerWidth > mobileWidthStop) return;
if (!e.target.closest('.hamburger-button') && !e.target.closest('.nav-links')) {
closeMenu();
}
});
// Close menu on resize to desktop
window.addEventListener('resize', function() {
if (window.innerWidth > mobileWidthStop) {
closeMenu();
}
});
});

View File

@ -0,0 +1,203 @@
// Search functionality
document.addEventListener('DOMContentLoaded', function() {
if (typeof PagefindModularUI === 'undefined') {
console.error('PagefindModularUI library not loaded');
return;
}
class El {
constructor(tagname) {
this.element = document.createElement(tagname);
}
id(s) {
this.element.id = s;
return this;
}
class(s) {
this.element.classList.add(s);
return this;
}
attrs(obj) {
for (const [k, v] of Object.entries(obj)) {
this.element.setAttribute(k, v);
}
return this;
}
text(t) {
this.element.innerText = t;
return this;
}
html(t) {
this.element.innerHTML = t;
return this;
}
handle(e, f) {
this.element.addEventListener(e, f);
return this;
}
addTo(el) {
if (el instanceof El) {
el.element.appendChild(this.element);
} else {
el.appendChild(this.element);
}
return this.element;
}
}
function getLink(location, anchors, url) {
if (!anchors || !anchors.length)
return null;
// find the closest anchor at or before the current location
const anchor = anchors.sort((a, b) => b.location - a.location).find(a => a.location <= location);
if (anchor) {
return url + "#" + anchor.id;
}
return null;
}
const resultTemplate = (result) => {
const wrapper = new El("li").class("pagefind-modular-list-result");
wrapper.handle("click", closeResults);
const thumb = new El("div").class("pagefind-modular-list-thumb").addTo(wrapper);
let image = result?.meta?.image;
if (image) {
if (image.includes("/_images/"))
image = image.substring(image.indexOf("/_images/"));
new El("img").class("pagefind-modular-list-image").attrs({
src: image,
alt: result.meta.image_alt || result.meta.title
}).addTo(thumb);
}
const inner = new El("div").class("pagefind-modular-list-inner").addTo(wrapper);
const title = new El("p").class("pagefind-modular-list-title").addTo(inner);
new El("a").class("pagefind-modular-list-link").text(result.meta?.title).attrs({
href: result.meta?.url || result.url
}).addTo(title);
const excerpt = new El("p").class("pagefind-modular-list-excerpt").addTo(inner);
const locations = result.weighted_locations.sort((a, b) => b.weight - a.weight);
const url = getLink(locations[0]?.location, result.anchors, result.url) || result.meta?.url || result.url;
new El("a").class("pagefind-modular-list-link").html(result.excerpt).attrs({
href: url
}).addTo(excerpt);
return wrapper.element;
}
// Create search input and container
const searchContainer = document.getElementById('nav-search-container');
// Create search input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = "frontpage-search";
searchInput.placeholder = 'Search...';
searchInput.className = 'pagefind-ui__search-input';
searchContainer.appendChild(searchInput);
const resultsContainer = document.getElementById('nav-search-results');
const instance = new PagefindModularUI.Instance({
showSubResults: true,
showImages: false,
resetStyles: true,
ranking: {
pageLength: 0.0,
termSaturation: 1.6,
termFrequency: 0.4,
termSimilarity: 6.0
}
});
// Add input component
instance.add(new PagefindModularUI.Input({
inputElement: "#frontpage-search"
}));
// Add results component
instance.add(new PagefindModularUI.ResultList({
containerElement: "#nav-search-results",
resultTemplate: resultTemplate
}));
let top_hit = null;
function closeResults() {
resultsContainer.style.display = 'none';
top_hit = null;
}
// Show/hide results
instance.on("results", async (results) => {
if (results.results.length) {
resultsContainer.style.display = 'block';
const data = await results.results[0].data();
top_hit = data.url;
} else {
closeResults();
}
});
document.addEventListener('click', function (e) {
if (!e.target.closest('#nav-search-results')) {
closeResults();
}
});
// Create clear button
const clearButton = document.createElement('button');
clearButton.type = 'button';
clearButton.className = 'search-clear-button';
clearButton.textContent = "X";
clearButton.style.display = 'none';
searchContainer.appendChild(clearButton);
// Show/hide clear button based on input content
searchInput.addEventListener('input', () => {
clearButton.style.display = searchInput.value.length > 0 ? 'flex' : 'none';
});
// Clear search when button is clicked
clearButton.addEventListener('click', () => {
searchInput.value = '';
clearButton.style.display = 'none';
instance.triggerSearch('');
resultsContainer.style.display = 'none';
searchInput.focus(); // Re-focus the search box after clearing
});
document.addEventListener('keydown', function (event) {
if (!(searchInput === document.activeElement) && event.key === '/') { // Use '/' key as trigger
event.preventDefault(); // Prevent the '/' key from being entered in the search box
if (isMobile)
openMenu();
searchInput.focus();
}
});
const navContainer = document.getElementById('nav-container');
searchInput.addEventListener('focusin', () => {
navContainer.style.transform = `translateY(0)`;
});
searchInput.addEventListener('beforeinput', () => {
navContainer.style.transform = `translateY(0)`;
});
searchInput.addEventListener('keydown', function (event) {
if (event.key === "Enter" && !!top_hit) {
window.location = top_hit;
top_hit = null;
}
});
});

View File

@ -0,0 +1,14 @@
<div class="code-block">
<button class="copy-button" aria-label="Copy code">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span class="copy-feedback"></span>
</button>
<div class="codeblock-content">
{{- $result := transform.HighlightCodeBlock . -}}
{{- $result.Wrapped -}}
</div>
</div>
{{ .Page.Store.Set "hasCode" true }}

View File

@ -0,0 +1,4 @@
<h{{ .Level }} id="{{ .Anchor | safeURL }}">
{{ .Text | safeHTML }}
<img data-pagefind-ignore="all" alt="Copy link to header" class="copy-link dark-invert" data-anchor="{{ .Anchor }}" title="Copy link" src="/images/link.svg">
</h{{ .Level }}>

View File

@ -0,0 +1,40 @@
{{- $link := .Destination -}}
{{- $text := .Text | safeHTML -}}
{{- if strings.HasPrefix $link "#" -}}
{{- $id := strings.TrimPrefix "#" $link -}}
{{- $anchors := site.Data.anchors -}}
{{- $entries := index $anchors $id -}}
{{- $foundLocal := false -}}
{{- if $entries -}}
{{- range $entries -}}
{{- if eq .page .Page.File.Path -}}
{{- $foundLocal = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $foundLocal -}}
<a href="{{ $link }}"{{ with .Title }} title="{{ . }}"{{ end }}>{{ $text }}</a>
{{- else if $entries -}}
{{- $firstEntry := index $entries 0 -}}
<a href="/{{ $firstEntry.page }}#{{ $id }}"{{ with .Title }} title="{{ . }}"{{ end }}>{{ $text }}</a>
{{- else -}}
{{- if eq hugo.Environment "development" -}}
{{ warnf "Unresolved anchor '%s' in page '%s'" $link .Page.File.Path }}
<a href="{{ $link }}" class="unresolved-anchor"{{ with .Title }} title="{{ . }}"{{ end }}>{{ $text }}</a>
{{- end -}}
{{- if eq hugo.Environment "production" -}}
{{ errorf "Unresolved anchor '%s' in page '%s'" $link .Page.File.Path }}
{{- end -}}
{{- end -}}
{{- else -}}
<a href="{{ $link }}"{{ with .Title }} title="{{ . }}"{{ end }}>{{ $text }}
{{- $u := urls.Parse .Destination -}}
{{- if $u.IsAbs -}}
<img data-pagefind-ignore="all" alt="External link" class="external-link dark-invert" title="External link" src="/images/external-link.svg">
{{- end -}}
</a>
{{- end -}}
{{- /**/ -}}

View File

@ -0,0 +1,18 @@
{{/*
MATH SHORTCODE
Formats equations
Usage:
{{< math >}}
c = \\pm\\sqrt{a^2 + b^2}"
{{< /math >}}
Content:
The content between the opening and closing shortcode tags will be formatted as maths
*/}}
{{ $opts := dict "output" "htmlAndMathml" }}
{{ if eq .Type "block" }}
{{ $opts = merge $opts (dict "displayMode" true) }}
{{ end }}
{{ transform.ToMath .Inner $opts }}
{{ .Page.Store.Set "hasMath" true }}

View File

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode | default "en" }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{ if .IsHome }}
{{ partial "site-verification.html" . }}
{{ end }}
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
{{ $seo := .Page.Param "seo" }}
{{ with $seo }}
{{ $src := $seo.image }}
{{ with $seo.description | default .Page.Title | default .Site.Params.Description }}
<meta name="description" content="{{ . }}">
<meta name="twitter:description" content="{{ . }}">
<meta property="og:description" content="{{ . }}">
{{ end }}
{{ if $src }}
{{/* Check if image exists in local images directory first */}}
{{- $localPath := printf "images/%s" $src -}}
{{- $componentPath := printf "%s/images/%s" (path.Dir .Page.File.Path) $src -}}
{{- $globalPath := printf "images/%s" $src -}}
{{- $imagePath := "" -}}
{{- if fileExists (printf "content/%s" $componentPath) -}}
{{- $imagePath = $componentPath -}}
{{- else if fileExists (printf "static/%s" $globalPath) -}}
{{- $imagePath = $globalPath -}}
{{- else -}}
{{- $imagePath = $src -}}
{{- end -}}
{{ with $imagePath }}
<meta property="og:image" content="{{ . | absURL }}">
<meta name="twitter:image" content="{{ . | absURL }}">
{{ end }}
{{ end }}
{{ end }}
<meta property="og:title" content="{{ .Page.Title }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ .Page.Permalink }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Page.Title }}">
<script data-cfasync="false" type="text/javascript">
// cfasync is set false to prevent Cloudflare from modifying the script
// Critical theme preferences
// Theme handling
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', savedTheme);
</script>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="{{ "/images/logo.svg" | relURL }}">
<!-- CSS -->
{{ partial "asset.html" (dict "path" "css/main.css" "defer" false) }}
{{ partial "asset.html" (dict "path" "css/tables.css" "defer" true) }}
{{ partial "asset.html" (dict "path" "pagefind/pagefind-modular-ui.css" "defer" true) }}
{{ block "head" . }}{{ end }}
</head>
<body class="body branch-{{ .Site.Params.Branch }}">
<div class="page-container">
<div class="nav-container" id="nav-container">
<nav class="sticky-nav">
{{ if and (ne .IsHome true) (.Content) }}
<button type="button" class="toc-button" id="toc-toggle" aria-label="Open table of contents"
aria-expanded="false">
{{ os.ReadFile "static/images/icons/list.svg" | safeHTML }}
</button>
{{ end }}
<div class="nav-logo">
<a id="nav-logo-a"
{{ if or (eq .Page.Title "ESPHome Docs") .IsHome }}
title="ESPHome Landing Page"
href="{{ "/" | relURL }}">
{{ else }}
title="ESPHome Documentation Home"
href="{{ "/components" | relURL }}">
{{ end }}
{{ os.ReadFile "static/images/logo-text.svg" | safeHTML }}
</a>
</div>
<button type="button" class="hamburger-button" aria-label="Open menu" aria-expanded="false">
<span></span>
<span></span>
<span></span>
</button>
<div class="nav-links">
<div class="dropbtn">
<button class="theme-toggle" aria-label="Toggle theme" title="Toggle dark mode">
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24"
fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
{{ range (index .Site.Menus "main") }}
{{ if .HasChildren }}
<div class="dropdown">
<button type="button" class="dropbtn" aria-haspopup="true"
aria-expanded="false">{{ .Name }}</button>
<div class="dropdown-content">
{{ range .Children }}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</div>
</div>
{{ else }}
<a class="nav-link" href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
{{ end }}
<div class="nav-search">
<div id="nav-search-container"></div>
<div id="nav-search-results"></div>
</div>
</div>
</nav>
</div>
<main data-pagefind-body>
{{ block "main" . }}{{ end }}
<div class="branch-overlay">{{ .Site.Params.Branch | upper }}</div>
</main>
<footer>
<div class="footer-content">
<a class="footer-logo" href="https://www.openhomefoundation.org/">
<span>ESPHome is a project from the</span>
<img src="{{ "images/ohf-logo-on-dark.svg" | relURL }}" alt="ESPHome Logo" width="220px" height="49px">
</a>
<div class="footer-links">
{{ range (index .Site.Menus "footer") }}
<div class="footer-column">
<div class="footer-heading">{{ .Name }}</div>
<ul>
{{ range .Children }}
<li><a href="{{ .URL | relURL }}">
{{ os.ReadFile (printf "static/images/icons/%s.svg" .Post) | safeHTML }}
{{ .Name }}
</a></li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
</div>
<div class="build-info">
<button id="build-info-button" class="build-info-button" title="Show build information">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12" y2="8"></line>
</svg>
</button>
</div>
<div id="build-info-popup" class="build-info-popup">
<div class="build-info-content">
<span class="build-info-close">&times;</span>
<h4>Build Information</h4>
<div class="build-info-grid">
<div class="build-info-label">Branch:</div>
<div class="build-info-value">{{ .Site.Params.branch | default "unknown" }}</div>
{{ $repo := strings.TrimSuffix ".git" .Site.Data.repo.url }}
<div class="build-info-label">Commit hash:</div>
<div class="build-info-value"><a href="{{ $repo }}/commit/{{ .Site.Params.commit.hash }}"
target="_blank">{{ .Site.Params.commit.hash }}</a></div>
<div class="build-info-label">Message:</div>
<div class="build-info-value">{{ .Site.Params.commit.title }}</div>
<div class="build-info-label">Build date:</div>
<div class="build-info-value">{{ now.Format "2006-01-02" }}</div>
</div>
</div>
</div>
</footer>
</div>
<!-- JavaScript -->
<!-- if there are equations on the page, use katex to render client-side -->
{{ if .Store.Get "hasMath" }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css"
integrity="sha384-5TcZemv2l/9On385z///+d7MSYlvIEw9FuZTIdZ14vJLqWphw7e7ZPuOiCHJcFCP" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"
integrity="sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"
integrity="sha384-hCXGrW6PitJEwbkoStFjeJxv+fSOOQKOPbJxSfM6G5sWZjAyWhXiTIIAmQqnlLlh" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
{{ end }}
{{ partial "asset.html" (dict "path" "/js/lazysizes.min.js" "defer" true) }}
{{ partial "asset.html" (dict "path" "/pagefind/pagefind-modular-ui.js" "defer" true) }}
{{ partial "asset.html" (dict "path" "/js/menu.js" "defer" true) }}
{{ partial "asset.html" (dict "path" "/js/search.js" "defer" true) }}
{{ partial "asset.html" (dict "path" "/js/main.js" "defer" true) }}
<!-- Build info popup script -->
{{ if .Store.Get "hasCode" }}
{{ partial "asset.html" (dict "path" "css/chroma.css" "defer" true) }}
{{ end }}
{{ block "scripts" . }}{{ end }}
</body>
</html>

View File

@ -0,0 +1,24 @@
{{ define "main" }}
<div class="content-container">
{{ if .Content }}
<div class="sidebar sidebar-fixed" id="sidebar">
{{ partial "sidebar.html" . }}
</div>
<div class="content">
{{ if not ( eq .Title "ESPHome Docs") }}
<h1>{{ .Title }}</h1>
{{ end }}
{{ .Content }}
</div>
<div class="overlay" id="overlay"></div>
<div class="sidebar sidebar-mobile">
{{ partial "sidebar.html" . }}
</div>
{{ else }}
<div class="content">
<h1>{{ .Title }}</h1>
{{ partial "sidebar.html" . }}
</div>
{{ end }}
</div>
{{ end }}

View File

@ -0,0 +1,24 @@
{{ define "main" }}
<div class="content-container">
<div class="sidebar sidebar-fixed">
{{ partial "sidebar.html" . }}
</div>
<div class="content">
{{ $firstHeading := "" }}
{{ with (index (index .Fragments.Headings 0).Headings 0) }}
{{ $firstHeading = .Title }}
{{ end }}
{{ if ne .Title $firstHeading }}
<h1>{{ .Title }}</h1>
{{ end }}
{{ .Content }}
</div>
<div class="overlay" id="overlay"></div>
<div class="sidebar sidebar-mobile">
{{ partial "sidebar.html" . }}
</div>
</div>
{{ end }}

View File

@ -0,0 +1,23 @@
{{ define "main" }}
<div class="hero-container">
<div class="hero-content">
<h1>Smart Home Made Simple</h1>
<p>Turn your ESP32, ESP8266, or RP2040 boards into powerful smart home devices with simple YAML configuration</p>
</div>
<div class="hero-img">
{{ partial "image.html" (dict "src" "hero.png" "page" .Page "thumbSize" "300x q25") }}
</div>
</div>
<div class="cta-container">
<div class="cta-content">
<div class="cta-buttons">
<a href="{{ "guides/getting_started_hassio/" | relURL }}" class="btn btn-primary">Installation Guide</a>
<a href="{{ "components/" | relURL }}" class="btn btn-secondary">Browse Components</a>
<a href="https://devices.esphome.io/" class="btn btn-secondary">Device Database</a>
</div>
</div>
</div>
{{ .Content }}
{{ end }}

View File

@ -0,0 +1,26 @@
{{- $path := .path -}}
{{- $defer := .defer -}}
{{- $ext := path.Ext $path | lower -}}
{{- if eq hugo.Environment "development" -}}
{{- $asset := resources.Get $path -}}
{{- if eq $ext ".css" -}}
<link rel="stylesheet" href="{{ $asset.RelPermalink }}"
>
{{- else if eq $ext ".js" -}}
<script src="{{ $asset.RelPermalink }}"></script>
{{- end -}}
{{- else -}}
{{- $asset := resources.Get $path | resources.Minify | resources.Fingerprint "sha256" -}}
{{- if eq $ext ".css" -}}
<link rel="stylesheet" href="{{ $asset.RelPermalink }}" integrity="{{ $asset.Data.Integrity }}"
{{ if $defer }}
media="(max-width:1px)"
onload="this.onload=null;this.removeAttribute('media');"
fetchpriority="high"
{{ end }}
>
{{- else if eq $ext ".js" -}}
<script src="{{ $asset.RelPermalink }}" integrity="{{ $asset.Data.Integrity }}" defer></script>
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,13 @@
{{ $currentPage := . }}
<ul>
{{ range where .Site.Pages "Section" "automations" }}
{{ if .IsSection }}
{{ continue }}
{{ end }}
<li>
<a href="{{ .RelPermalink }}" class="{{ if eq $currentPage.RelPermalink .RelPermalink }}active{{ end }}">
{{ .Title }}
</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1,13 @@
{{ $currentPage := . }}
<ul>
{{ range where .Site.Pages "Section" "changelog" }}
{{ if .IsSection }}
{{ continue }}
{{ end }}
<li>
<a href="{{ .RelPermalink }}" class="{{ if eq $currentPage.RelPermalink .RelPermalink }}active{{ end }}">
{{ .Title }}
</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1,13 @@
{{ $currentPage := . }}
<ul>
{{ range where .Site.Pages "Section" "components" }}
{{ if .IsSection }}
{{ continue }}
{{ end }}
<li>
<a href="{{ .RelPermalink }}" class="{{ if eq $currentPage.RelPermalink .RelPermalink }}active{{ end }}">
{{ .Title }}
</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1,13 @@
{{ $currentPage := . }}
<ul>
{{ range where .Site.Pages "Section" "guides" }}
{{ if .IsSection }}
{{ continue }}
{{ end }}
<li>
<a href="{{ .RelPermalink }}" class="{{ if eq $currentPage.RelPermalink .RelPermalink }}active{{ end }}">
{{ .Title }}
</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1,78 @@
{{ $ext := lower (path.Ext .src) }}
{{ $page := .page }}
{{ $style := .style }}
{{ $thumbSize := .thumbSize | default "180x q20" }}
{{/* Check if image exists in local images directory first */}}
{{ $src := strings.TrimPrefix "/images/" .src }}
{{ $src = strings.TrimPrefix "images/" $src }}
{{- $componentPath := printf "content/%s/images/%s" (path.Dir $page.File.Path) $src -}}
{{- $globalPath := printf "images/%s" $src -}}
{{ $image := resources.Get $componentPath }}
{{ if not $image }}
{{ $image = resources.Get $globalPath }}
{{ end }}
{{ if not $image }}
<pre>{{ .src }}; {{ $ext }}; {{ $componentPath }} {{ $globalPath}}</pre>
{{ end }}
{{ if not $image }}
{{ warnf "Unresolved image '%s' in page '%s'" .src .Page.File.Path }}
{{ end }}
{{- if and $image (eq $ext ".svg") -}}
<img src="{{ $image.RelPermalink }}" {{ if .width }}width="{{.width}}" {{ end }} alt="{{ .alt }}" {{ $style }}/>
{{ end }}
{{ if and $image (in (split ".jpeg,.jpg,.png" ",") $ext) }}
{{ if isset . "fill" }}
{{ $image = $image.Fill .fill }}
{{ end }}
{{ $placeholder := ($image.Resize $thumbSize) | images.Filter (images.GaussianBlur 1) }}
{{ $src_set := slice (print $image.RelPermalink " " $image.Width "w") }}
{{ $maxwidth := .maxwidth | default 1500 }}
<!-- Generate a range of image sizes to allow adaptive loading -->
{{ range $wid := slice (div $maxwidth 5) (div $maxwidth 3) (div $maxwidth 2) (div $maxwidth 1.5) $maxwidth }}
{{ $iwid := int $wid }}
{{ if ge $image.Width $iwid}}
{{ $i := $image.Resize (print $iwid "x" ) }}
{{ $src_set = $src_set | append (print $i.RelPermalink " " $iwid "w") }}
{{ end }}
{{ end }}
<noscript>
<style>
figure.lazy {
display: none;
}
</style>
<figure class="{{ .classes }}">
{{ if .href }}
<a href='{{ .href }}'>
{{ end }}
<img src="{{ $image.RelPermalink }}" {{ if .width }}width="{{.width}}" {{ end }} alt="{{ .alt }}"/>
{{ if .href }}
</a>
{{ end }}
<figcaption>
<em>{{ .Inner }}</em>
</figcaption>
</figure>
</noscript>
{{ $srclist := delimit $src_set ", " }}
<figure class="{{ .classes }} lazy">
{{ if .href }}
<a href='{{ .href }}'>
{{ end }}
<img class="lazyload w-100 h-100" data-sizes="auto" src="{{ $image.RelPermalink }}" {{ if .width }}width="{{.width}}" {{ end }}
fetchpriority=high
{{ $style }}
srcset="data:image/jpeg;base64,{{ $placeholder.Content | base64Encode }}"
data-src="{{ $image.RelPermalink }}"
data-srcset="{{ $srclist }}" style="max-width: {{ $image.Width }}px; max-height: {{ $image.Height }}px;"
width="{{ $image.Width }}" height="{{ $image.Height }}"
alt="{{ .alt }}"/>
{{ if .href }}
</a>
{{ end }}
</figure>
{{ end }}

View File

@ -0,0 +1,37 @@
{{/*
Try to convert a link text to match a known anchor, with soft failure.
The link will be searched for in the given domain, and or category
*/}}
{{- $text := .text -}}
{{- $domain := .domain -}}
{{- $category := .category -}}
{{- $link := .link -}}
{{- $links := slice (printf "%s%s" $domain .link) (printf "%s%s-%s" $domain .link $category) .link (printf "%s-%s" .link $category) -}}
{{- $anchors := site.Data.anchors -}}
{{- $entries := false -}}
{{- range $links -}}
{{- $entries = index $anchors . -}}
{{- if $entries -}}
{{- $link = . -}}
{{- break -}}
{{- end -}}
{{- end -}}
{{- $foundLocal := false -}}
{{- if $entries -}}
{{- range $entries -}}
{{- if eq .page .Page.File.Path -}}
{{- $text = $text | default .text -}}
{{- $foundLocal = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $foundLocal -}}
{{- printf "[%s](#%s)" $text $link -}}
{{- else if $entries -}}
{{- $firstEntry := index $entries 0 -}}
{{- $text = $text | default $firstEntry.text -}}
{{- printf "[%s](/%s#%s)" $text $firstEntry.page $link -}}
{{- else -}}
{{- $text -}}
{{- end -}}

View File

@ -0,0 +1,31 @@
{{- $branch := site.Data.repo.branch -}}
{{- $category := .Get 0 -}}
{{- $data := index site.Data.automations $branch "automations" -}}
{{- if $data -}}
{{- $markdown := "" -}}
{{- range $cat, $items := $data -}}
{{- if eq $cat $category -}}
{{- $singular := substr $category 0 (sub (len $category) 1) -}}
{{- range $domain, $entries := $items -}}
{{- $entryList := "" -}}
{{- $docref := printf "{{< docref %s %s true >}}" $domain $domain -}}
{{- if eq $domain "Core" -}}
{{- $docref = printf "[Common %s](#common-%s)" $category $category -}}
{{- $domain = "" -}}
{{- end -}}
{{- range $i, $entry := $entries -}}
{{- $entryLink := printf "%s" (replace $entry "." "") -}}
{{- $entryLink = partial "linkify.html" (dict "domain" $domain "text" $entry "link" $entryLink "category" $singular) -}}
{{- if $i -}}{{- $entryList = printf "%s, " $entryList -}}{{- end -}}
{{- $entryList = printf "%s%s" $entryList $entryLink -}}
{{- end -}}
{{- $markdown = printf "%s- **%s**: %s\n" $markdown $docref $entryList -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $markdown | markdownify -}}
{{- else -}}
<p>⚠️ Data not found for branch: <code>{{- $branch -}}</code></p>
{{- end -}}

View File

@ -0,0 +1,58 @@
{{ $currentPage := . }}
{{ $title := .Title }}
<div class="sidebar-content {{ if .Content }}scroll-trap{{ end }}">
<h3>{{ $title }}</h3>
<nav class="sidebar-nav">
{{ if .Fragments }}
{{ if .Fragments.Headings }}
{{ $headings := slice }}
<ul>
{{ $start_level := 9 }}
{{ range .Fragments.HeadingsMap }}
{{ if gt $start_level .Level }}
{{ $start_level = .Level }}
{{ end }}
{{ end }}
{{ range .Fragments.HeadingsMap }}
{{ if eq .Level $start_level }}
<li><a class="toc-entry" href="#{{ .ID }}">{{ .Title | safeHTML }}</a>
{{ if .Headings }}
<ul>
{{ range .Headings }}
{{ $headings := collections.Append .ID $headings }}
<li><a href="#{{ .ID }}">{{ .Title | safeHTML }}</a></li>
{{ end }}
</ul>
{{ end }}
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
{{ end }}
{{ if .Pages }}
<h3>In this section</h3>
<ul class="section-list">
{{ range .Pages }}
{{ if .Title }}
<li class="section-item">
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
</nav>
<div class="sidebar-resource">
<hr>
{{ $repo := strings.TrimSuffix ".git" .Site.Data.repo.url }}
{{ with .File }}
{{ $path := printf "%s/blob/current/content/%s" $repo .Path }}
<a href="{{ $path }}" class="ghedit">
{{ os.ReadFile "static/images/icons/edit.svg" | safeHTML }}
Edit this page on GitHub
</a>
{{ end }}
</div>
</div>

View File

@ -0,0 +1,12 @@
{{ if .Site.Params.seo.webmaster_verifications.google }}
<meta name="google-site-verification" content="{{ .Site.Params.seo.webmaster_verifications.google }}" />
{{ end }}
{{ if .Site.Params.seo.webmaster_verifications.bing }}
<meta name="msvalidate.01" content="{{ .Site.Params.seo.webmaster_verifications.bing }}">
{{ end }}
{{ if .Site.Params.seo.webmaster_verifications.alexa }}
<meta name="alexaVerifyID" content="{{ .Site.Params.seo.webmaster_verifications.alexa }}">
{{ end }}
{{ if .Site.Params.seo.webmaster_verifications.yandex }}
<meta name="yandex-verification" content="{{ .Site.Params.seo.webmaster_verifications.yandex }}">
{{ end }}

View File

@ -0,0 +1,11 @@
{{/*
ANCHOR SHORTCODE
Creates an HTML anchor point that can be linked to with fragment identifiers.
Usage:
{{< anchor "my-anchor-id" >}}
Parameters:
1. anchor ID (required) - The ID to use for the anchor point
*/}}
<a class="anchor" id="{{ .Get 0 }}"></a>

View File

@ -0,0 +1,115 @@
<style>
.api-key-input {
--api-input-bg: #fff;
--api-input-border: #b0b9be;
--api-input-text: #222;
--api-input-shadow: 0 1px 4px rgba(0,0,0,0.06);
--api-input-bg-focus: #f6fdff;
--api-input-border-focus: #00979d;
--api-copy-btn-bg: #f7fafd;
--api-copy-btn-text: #00979d;
--api-copy-btn-border: #b0b9be;
--api-copy-btn-bg-hover: #e0f7fa;
}
[data-theme="dark"] .api-key-input {
--api-input-bg: #232728;
--api-input-border: #333b3d;
--api-input-text: #eee;
--api-input-shadow: 0 1px 7px rgba(0,0,0,0.18);
--api-input-bg-focus: #1a2326;
--api-input-border-focus: #1abfc6;
--api-copy-btn-bg: #1a2326;
--api-copy-btn-text: #1abfc6;
--api-copy-btn-border: #333b3d;
--api-copy-btn-bg-hover: #233438;
}
.api-key-input {
display: flex;
gap: 0.5em;
align-items: center;
}
.api-key-input input[type="text"] {
background: var(--api-input-bg);
color: var(--api-input-text);
border: 1.5px solid var(--api-input-border);
border-radius: 6px;
padding: 0.5em 1em;
font-size: 1.05em;
font-family: inherit;
box-shadow: var(--api-input-shadow);
transition: border 0.15s, background 0.18s;
outline: none;
width: 490px;
max-width: 98vw;
margin-bottom: 0.5em;
}
.api-key-input input[type="text"]:focus {
background: var(--api-input-bg-focus);
border-color: var(--api-input-border-focus);
}
.api-key-input input[type="text"]:hover {
border-color: var(--api-input-border-focus);
}
.api-key-input button {
background: var(--api-copy-btn-bg);
color: var(--api-copy-btn-text);
border: 1.5px solid var(--api-copy-btn-border);
border-radius: 6px;
padding: 0.5em 1em;
font-size: 1em;
font-family: inherit;
cursor: pointer;
transition: background 0.18s, border 0.15s;
margin-bottom: 0.5em;
outline: none;
position: relative;
}
.api-key-input button:hover, .api-key-input button:focus {
background: var(--api-copy-btn-bg-hover);
border-color: var(--api-input-border-focus);
}
.api-key-input .copied {
color: var(--api-copy-btn-text);
font-size: 0.95em;
margin-left: 0.5em;
opacity: 1;
transition: opacity 0.3s;
user-select: none;
}
</style>
<div class="api-key-input">
<input type="text" id="api-key" onclick="this.focus();this.select()" readonly="readonly">
<button type="button" id="copy-api-key" aria-label="Copy API key">Copy</button>
<span class="copied" id="api-key-copied" style="display:none">Copied!</span>
</div>
<script>
// https://stackoverflow.com/a/62362724
function bytesArrToBase64(arr) {
const abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // base64 alphabet
const bin = n => n.toString(2).padStart(8,0); // convert num to 8-bit binary string
const l = arr.length;
let result = '';
for(let i=0; i<=(l-1)/3; i++) {
let c1 = i*3+1>=l; // case when "=" is on end
let c2 = i*3+2>=l; // case when "=" is on end
let chunk = bin(arr[3*i]) + bin(c1? 0:arr[3*i+1]) + bin(c2? 0:arr[3*i+2]);
let r = chunk.match(/.{1,6}/g).map((x,j)=> j===3&&c2 ? '=' :(j===2&&c1 ? '=':abc[+('0b'+x)]));
result += r.join('');
}
return result;
}
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);
document.getElementById("api-key").value = bytesArrToBase64(array);
document.getElementById("copy-api-key").addEventListener("click", function() {
var input = document.getElementById("api-key");
input.select();
input.setSelectionRange(0, 99999); // For mobile
navigator.clipboard.writeText(input.value).then(function() {
var copied = document.getElementById("api-key-copied");
copied.style.display = "inline";
setTimeout(function() { copied.style.display = "none"; }, 1200);
});
});
</script>

View File

@ -0,0 +1,36 @@
{{- /*
apiclass shortcode - Creates a link specifically to a C++ class in the API documentation
Usage:
{{< apiclass "DisplayName" "path/to/Class" >}}
Examples:
{{< apiclass "ClimateDevice" "esphome::climate::ClimateDevice" >}}
{{< apiclass "WiFiComponent" "esphome::wifi::WiFiComponent" >}}
How it works:
1. Extracts the last path element from the provided path
2. Replaces special characters:
- Underscores (_) are replaced with double underscores (__)
- Dots (.) are replaced with _8
- Colons (:) are replaced with _1
3. Converts capital letters to lowercase preceded by an underscore
4. Prepends the API documentation base URL (configured in Hugo settings)
When converting from RST, the following directive is automatically converted:
:apiclass:`DisplayName <path/to/Class>`
:apiclass:`esphome::climate::ClimateDevice` (uses path as display name)
*/ -}}
{{- $text := .Get 0 -}}
{{- $path := .Get 1 -}}
{{- $lastPathElement := index (last 1 (split $path "/")) 0 -}}
{{- $processedPath := replace (replace (replace $lastPathElement "_" "__") "." "_8") ":" "_1" -}}
{{ $match := findRE `([A-Z])` $processedPath }}
{{ range $match }}
{{ $original := . }}
{{ $lower := lower . | printf "_%s" }}
{{ $processedPath = replace $processedPath $original $lower }}
{{ end }}
{{- $baseUrl := .Site.Params.api_docs_url -}}
<a href="{{ $baseUrl }}/classesphome_1_1{{ $processedPath }}">{{ $text }}</a>

View File

@ -0,0 +1,39 @@
{{- /*
apiref shortcode - Creates a link to a C++ API reference
Usage:
{{< apiref "DisplayName" "path/to/api/element" >}}
Examples:
{{< apiref "Sensor class" "esphome::sensor::Sensor" >}}
{{< apiref "Component" "esphome/core/component.h::Component" >}}
How it works:
1. Extracts the last path element from the provided path
2. Replaces special characters:
- Underscores (_) are replaced with double underscores (__)
- Dots (.) are replaced with _8
- Colons (:) are replaced with _1
3. Converts capital letters to lowercase preceded by an underscore
4. Prepends the API documentation base URL (configured in Hugo settings)
When converting from RST, the following directive is automatically converted:
:apiref:`DisplayName <path/to/api/element>`
:apiref:`esphome::sensor::Sensor` (uses path as display name)
*/ -}}
{{- $text := .Get 0 -}}
{{- $path := .Get 1 -}}
{{- if eq $text $path -}}
{{- $text = "API Reference" }}
{{- end }}
{{- $lastPathElement := index (last 1 (split $path "/")) 0 -}}
{{- $processedPath := replace (replace (replace $lastPathElement "_" "__") "." "_8") ":" "_1" -}}
{{ $match := findRE `([A-Z])` $processedPath }}
{{ range $match }}
{{ $original := . }}
{{ $lower := lower . | printf "_%s" }}
{{ $processedPath = replace $processedPath $original $lower }}
{{ end }}
{{- $baseUrl := .Site.Params.api_docs_url -}}
<a href="{{ $baseUrl }}/{{ $processedPath }}">{{ $text }}</a>

View File

@ -0,0 +1,36 @@
{{- /*
apistruct shortcode - Creates a link specifically to a C++ struct in the API documentation
Usage:
{{< apistruct "DisplayName" "path/to/Struct" >}}
Examples:
{{< apistruct "SensorStateClass" "esphome::sensor::SensorStateClass" >}}
{{< apistruct "GPIOOutputPin" "esphome::output::GPIOOutputPin" >}}
How it works:
1. Extracts the last path element from the provided path
2. Replaces special characters:
- Underscores (_) are replaced with double underscores (__)
- Dots (.) are replaced with _8
- Colons (:) are replaced with _1
3. Converts capital letters to lowercase preceded by an underscore
4. Prepends the API documentation base URL (configured in Hugo settings)
When converting from RST, the following directive is automatically converted:
:apistruct:`DisplayName <path/to/Struct>`
:apistruct:`esphome::sensor::SensorStateClass` (uses path as display name)
*/ -}}
{{- $text := .Get 0 -}}
{{- $path := .Get 1 -}}
{{- $lastPathElement := index (last 1 (split $path "/")) 0 -}}
{{- $processedPath := replace (replace (replace $lastPathElement "_" "__") "." "_8") ":" "_1" -}}
{{ $match := findRE `([A-Z])` $processedPath }}
{{ range $match }}
{{ $original := . }}
{{ $lower := lower . | printf "_%s" }}
{{ $processedPath = replace $processedPath $original $lower }}
{{ end }}
{{- $baseUrl := .Site.Params.api_docs_url -}}
<a href="{{ $baseUrl }}/structesphome_1_1{{ $processedPath }}">{{ $text }}</a>

View File

@ -0,0 +1,20 @@
{{/*
BUTTON SHORTCODE
Creates a button with an image that links to a URL.
Usage:
{{< button href="https://example.com" img="/images/button.png" alt="Example Button" target="_self" >}}
Parameters:
- href (required) - The URL to link to
- img (required) - The path to the button image
- alt (optional) - Alt text for the image (default: "Button")
- target (optional) - Target attribute for the link (default: "_blank")
*/}}
{{- $href := .Get "href" -}}
{{- $img := .Get "img" -}}
{{- $alt := .Get "alt" | default "Button" -}}
{{- $target := .Get "target" | default "_blank" -}}
<a href="{{ $href }}" target="{{ $target }}">
<img src="{{ $img }}" alt="{{ $alt }}" />
</a>

View File

@ -0,0 +1,17 @@
{{/*
CAUTION SHORTCODE
Creates a caution admonition box to highlight important cautions or potential issues.
Usage:
{{< caution >}}
Incorrect wiring may damage your device. Double-check connections before powering on.
You can include **Markdown** formatting within the caution.
{{< /caution >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the caution box.
*/}}
<div class="admonition caution">
<p class="admonition-title">Caution</p>
{{ .Inner | .Page.RenderString}}
</div>

View File

@ -0,0 +1,30 @@
{{/*
COLLAPSE SHORTCODE
Creates a collapsible section with a title that can be clicked to show/hide content.
Usage:
{{< collapse "Optional Configuration" >}}
This content will be hidden by default and can be expanded by clicking the header.
You can include any Markdown content here, including lists, code blocks, etc.
{{< /collapse >}}
Parameters:
1. title (required) - The title text to display in the header
Content:
The content between the opening and closing shortcode tags will be hidden by default
and can be toggled by clicking on the header.
*/}}
{{ $title := .Get 0 }}
{{ $is_open := .Get 1 }}
<div class="collapse-container {{ if $is_open }}active{{ end }}">
<div class="collapse-header" onclick="this.parentElement.classList.toggle('active')">
<h4>{{ $title }}</h4>
<h5>Collapse</h5>
<span class="collapse-icon">+</span>
</div>
<div class="collapse-content">
{{ .Inner | markdownify }}
</div>
</div>

View File

@ -0,0 +1,121 @@
{{/*
DOCREF SHORTCODE
Creates a link to another page in the documentation with proper handling of anchors.
Usage:
{{< docref "components/sensor/dht" >}} <!-- Uses the target page title as link text -->
{{< docref "components/sensor/dht" "DHT Sensor Guide" >}} <!-- Uses custom text for the link -->
{{< docref "components/sensor/dht#configuration" >}} <!-- Links to a specific anchor on the page -->
Parameters:
1. path (required) - The path to the target page, can include an anchor with #
2. custom text (optional) - Custom text to use for the link (defaults to target page title)
3. An optional boolean, which if true will suppress the broken link message
Notes:
- The path should be relative to the content directory
- If the path is not absolute (doesn't start with /), it will try to resolve in this order:
1. Current page's directory
2. /components directory
3. Recursively in subfolders of /components
- If the target page doesn't exist, it will display a "broken link" message
*/}}
{{- $path := .Get 0 -}}
{{- $customText := .Get 1 -}}
{{- $brokenOK := .Get 2 -}}
{{- $anchor := "" -}}
{{- if findRE "#" $path 1 -}}
{{- $parts := split $path "#" -}}
{{- $path = index $parts 0 -}}
{{- $anchor = index $parts 1 -}}
{{- end -}}
{{/* Path resolution logic */}}
{{- $page := .Page.GetPage $path -}}
{{/* If page not found and path is not absolute, try current directory */}}
{{- if and (not $page) (not (hasPrefix $path "/")) -}}
{{- $currentDir := path.Dir .Page.File.Path -}}
{{- $newPath := path.Join $currentDir $path -}}
{{- $page = .Page.GetPage $newPath -}}
{{- end -}}
{{/* If still not found, try in components directory */}}
{{- if and (not $page) (not (hasPrefix $path "/")) -}}
{{- $componentsPath := path.Join "components" $path -}}
{{- $page = .Page.GetPage $componentsPath -}}
{{- end -}}
{{/* If still not found, try to find recursively in component subdirectories */}}
{{- if and (not $page) (not (hasPrefix $path "/")) -}}
{{/* Get all component pages */}}
{{- $componentSection := .Site.GetPage "section" "components" -}}
{{- if $componentSection -}}
{{/* Recursive function to search in component pages */}}
{{- $findInSection := false -}}
{{- range $componentSection.Sections -}}
{{/* Check if the current section has the page */}}
{{- $sectionPath := path.Join .File.Dir $path -}}
{{- $foundPage := $.Page.GetPage $sectionPath -}}
{{- if $foundPage -}}
{{- $page = $foundPage -}}
{{- $findInSection = true -}}
{{- end -}}
{{/* If not found, check in the section's pages */}}
{{- if not $findInSection -}}
{{- range .Pages -}}
{{- if eq (path.Base .File.Path | replaceRE "\\.(md|html)$" "") (path.Base $path | replaceRE "\\.(md|html)$" "") -}}
{{- $page = . -}}
{{- $findInSection = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* If still not found, check in subsections recursively */}}
{{- if not $findInSection -}}
{{- range .Sections -}}
{{- $sectionPath := path.Join .File.Dir $path -}}
{{- $foundPage := $.Page.GetPage $sectionPath -}}
{{- if $foundPage -}}
{{- $page = $foundPage -}}
{{- $findInSection = true -}}
{{- end -}}
{{/* Check in the subsection's pages */}}
{{- if not $findInSection -}}
{{- range .Pages -}}
{{- if eq (path.Base .File.Path | replaceRE "\\.(md|html)$" "") (path.Base $path | replaceRE "\\.(md|html)$" "") -}}
{{- $page = . -}}
{{- $findInSection = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $page -}}
{{- $displayText := $customText -}}
{{- if not $displayText -}}
{{- $displayText = $page.Title -}}
{{- end -}}
{{- if $anchor -}}
<a href="{{- $page.RelPermalink -}}#{{ $anchor }}">{{ $displayText }}</a>
{{- else -}}
<a href="{{- $page.RelPermalink -}}">{{ $displayText }}</a>
{{- end -}}
{{- else -}}
{{- if $brokenOK -}}
<span>{{- $customText -}}</span>
{{- else -}}
{{- if eq hugo.Environment "development" -}}
{{ warnf "Unresolved anchor '%s' in page '%s'" $path .Page.File.Path }}
<a href="{{ $path }}" class="unresolved-anchor"{{ with .Title }} title="{{ . }}"{{ end }}>{{ $customText }}</a>
{{- else -}}
{{ errorf "Unresolved anchor '%s' in page '%s'" $path .Page.File.Path }}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,17 @@
{{ $features := .Inner | transform.Unmarshal }}
<div class="feature-grid">
{{ range $features }}
<div class="feature-card">
<div class="feature-icon">
{{ if hasPrefix .icon "fa-" }}
<i aria-hidden="true" class="fas {{ .icon }}"></i>
{{ else }}
{{ os.ReadFile (printf "static/images/icons/%s.svg" .icon) | safeHTML }}
{{ end }}
</div>
<div class="feature-text">{{ .title }}</div>
<p>{{ .description | safeHTML }}</p>
</div>
{{ end }}
</div>

View File

@ -0,0 +1,27 @@
{{ $items := .Inner | transform.Unmarshal }}
<div class="getting-started-grid">
{{ range $items }}
<div class="feature-card">
<div class="feature-icon">
{{ if hasPrefix .icon "fa-" }}
<i aria-hidden="true" class="fas {{ .icon }}"></i>
{{ else }}
{{ os.ReadFile (printf "static/images/icons/%s.svg" .icon) | safeHTML }}
{{ end }}
</div>
<div class="getting-started-heading">{{ .title }}</div>
<p>{{ .description | safeHTML }}</p>
{{ if .steps }}
<ol>
{{ range .steps }}
<li>{{ . | safeHTML }}</li>
{{ end }}
</ol>
{{ end }}
{{ if .url }}
<a href="{{ .url }}" class="btn btn-primary">{{ .button_text }}</a>
{{ end }}
</div>
{{ end }}
</div>

View File

@ -0,0 +1,9 @@
{{/*
Shortcode: ghuser
Usage: {{< ghuser name="octocat" >}}
Output: A link to the specified GitHub user profile, displaying the username (or custom text if provided).
Optional param: text (if you want to show something different than the username)
*/}}
{{- $name := .Get "name" -}}
{{- $text := .Get "text" | default (printf "@%s" $name) -}}
<a href="https://github.com/{{ $name }}" target="_blank" rel="noopener noreferrer">{{ $text }}</a>

View File

@ -0,0 +1,22 @@
{{/*
HTML_FILE SHORTCODE
Read a file and insert as html inside a div
Usage:
{{< html_file file="example.html" class="examlple-class" >}}
Parameters:
- file (required) - The file to read and insert as html
- class (optional) - CSS class to apply to the enclosing div
*/}}
{{- $file := .Get "file" -}}
{{- $class := .Get "class" -}}
{{- if $class -}}
<div class="{{ $class }}">
{{- end -}}
{{ $content := os.ReadFile (printf "static/%s" $file) | safeHTML }}
{{- $content -}}
{{- if $class -}}
</div>
{{- end -}}

View File

@ -0,0 +1,34 @@
{{/*
IMG SHORTCODE
Displays an image with optional caption, width, height, and CSS class.
Automatically searches for images in the local component directory, global images directory, or uses absolute URLs.
Usage:
{{< img src="example.jpg" alt="Example image" caption="This is an example" width="500" class="center" >}}
Parameters:
- src (required) - The image source path or URL
- alt (optional) - Alt text for the image (default: "Image")
- caption (optional) - Caption text to display below the image
- class (optional) - CSS class to apply to the figure element
- width (optional) - Width of the image
- height (optional) - Height of the image
Notes:
- The shortcode will first look for images in the component's own images directory
- If not found there, it will look in the global images directory
- If still not found, it will use the src as provided (for external URLs)
*/}}
{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" | default "Image" -}}
{{- $caption := .Get "caption" -}}
{{- $class := .Get "class" -}}
{{- $width := .Get "width" -}}
{{- $height := .Get "height" -}}
{{- $style := "" -}}
{{- if $width -}}{{- $style = printf "%swidth:%spx;" $style $width -}}{{- end -}}
{{- if $height -}}{{- $style = printf "%sheight:%spx;" $style $height -}}{{- end -}}
{{- if $style -}}{{- $style = printf " style=\"%s\"" $style | safeHTMLAttr -}}{{- end -}}
{{ partial "image.html" (dict "src" $src "page" .Page "style" $style "width" $width) }}
{{- .Page.Store.Set "hasImg" true -}}

View File

@ -0,0 +1,79 @@
{{/*
IMGTABLE SHORTCODE
Creates a grid of component cards with images, titles, and optional descriptions that link to other pages.
Usage with block content (preferred):
{{< imgtable >}}
Title 1, path/to/page1, image1.png
Title 2, path/to/page2, image2.png, dark-invert
{{< /imgtable >}}
Legacy usage with positional parameters:
{{< imgtable "Title" "/path/to/page" "image.png" "css-class" >}}
Notes:
- Each line in the block content should contain 3-4 comma-separated values
- The format is: Title, Link, Image, [Optional CSS class]
- Images are searched for in the following order:
1. Component's own images directory
2. Global images directory
3. Used as-is (for external URLs)
- For dark mode compatibility, add "dark-invert" as the CSS class to invert the image in dark mode
*/}}
<div class="component-grid">
{{ if .Inner }}
{{ $opts := dict "targetType" "slice" "delimiter" "," "lazyQuotes" true }}
{{ $data := transform.Unmarshal $opts .Inner }}
{{ range $data }}
{{ $title := trim (index . 0) " " }}
{{ $link := trim (index . 1) " " | strings.TrimSuffix "index" }}
{{ $image := trim (index . 2) " " }}
{{ $class := trim (index . 3) " " }}
{{- $style := "" -}}
{{- if $class -}}{{- $style = printf " class=\"%s\"" $class | safeHTMLAttr -}}{{- end -}}
<div class="component-card">
<a href="{{ $link | relURL }}">
<div class="component-icon">
{{ partial "image.html" (dict "src" $image "page" $.Page "style" $style "alt" $title) }}
</div>
<div class="component-name">{{ $title }}</div>
</a>
</div>
{{ end }}
{{ else }}
{{ $title := .Get 0 }}
{{ $link := .Get 1 | strings.TrimSuffix "index" }}
{{ $image := .Get 2 }}
{{ $class := .Get 3 }}
{{- $style := "" -}}
{{- if $class -}}{{- $style = printf " class=\"%s\"" $class | safeHTMLAttr -}}{{- end -}}
{{/* Image path handling similar to img shortcode */}}
{{ $componentPath := printf "%s/images/%s" (path.Dir .Page.File.Path) $image }}
{{ $globalPath := printf "images/%s" $image }}
{{ $imagePath := "" }}
{{ if fileExists $componentPath }}
{{ $imagePath = $componentPath }}
{{ else if fileExists (printf "static/%s" $globalPath) }}
{{ $imagePath = $globalPath }}
{{ else }}
{{ $imagePath = $image }}
{{ end }}
<div class="component-card">
<a href="{{ $link | relURL }}">
<div class="component-icon">
{{ partial "image.html" (dict "src" $image "page" $.Page "style" $style "alt" $title) }}
</div>
<div class="component-name">{{ $title }}</div>
</a>
</div>
{{ end }}
</div>
</style>

View File

@ -0,0 +1,17 @@
{{/*
Important SHORTCODE
Creates an "important" admonition box to highlight important cautions or potential issues.
Usage:
{{< important >}}
Incorrect wiring may damage your device. Double-check connections before powering on.
You can include **Markdown** formatting within the block.
{{< /important >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the block.
*/}}
<div class="admonition important">
<p class="admonition-title">Important</p>
{{ .Inner | .Page.RenderString}}
</div>

View File

@ -0,0 +1,18 @@
{{/*
MATH SHORTCODE
Formats equations
Usage:
{{< math >}}
c = \\pm\\sqrt{a^2 + b^2}"
{{< /math >}}
Content:
The content between the opening and closing shortcode tags will be formatted as maths
*/}}
{{ $nl := printf "\n" }}
$$
{{ .Inner }}
$$
{{ .Page.Store.Set "hasMath" true }}

View File

@ -0,0 +1,17 @@
{{/*
NOTE SHORTCODE
Creates a note admonition box to highlight important information.
Usage:
{{< note >}}
This is important information that the reader should pay attention to.
You can include **Markdown** formatting within the note.
{{< /note >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the note box.
*/}}
<div class="admonition note">
<p class="admonition-title">Note</p>
{{ .Inner | .Page.RenderString }}
</div>

View File

@ -0,0 +1,16 @@
{{/*
OPTION SHORTCODE
Creates an option block
Usage:
{{< option "--help|-h" >}}
This is the help option.
{{< /option >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the option block.
*/}}
<div class="option">
<div class="option-title"><code>{{ .Get 0 }}</code></div>
{{ .Inner | .Page.RenderString }}
</div>

View File

@ -0,0 +1,9 @@
{{/*
Shortcode: pr
Usage: {{< pr number="123" repo="esphome" >}}
Output: A link to the specified pull request
Optional param: repo (default: "esphome")
*/}}
{{- $number := .Get "number" -}}
{{- $repo := .Get "repo" | default "esphome" -}}
<a href="https://github.com/esphome/{{ $repo }}/pull/{{ $number }}" target="_blank" rel="noopener noreferrer">{{ $repo }}#{{ $number }}</a>

View File

@ -0,0 +1,8 @@
{{/*
redirect shortcode: usage {{< redirect url="/some/path" >}}
Outputs a meta refresh and canonical link for SEO-friendly redirects.
*/}}
{{ $url := .Get "url" | strings.TrimSuffix "index.html" | strings.TrimSuffix ".html" }}
<meta http-equiv="refresh" content="0; url={{ $url }}">
<link rel="canonical" href="{{ $url }}">
<p>You are being redirected to <a href="{{ $url }}">{{ $url }}</a>...</p>

View File

@ -0,0 +1 @@
{{ partial "render-automations.html" . }}

View File

@ -0,0 +1,41 @@
{{/*
SEO SHORTCODE
Adds SEO metadata tags to the page for better search engine optimization and social media sharing.
This shortcode should be placed in the head section of your page or template.
Usage:
{{< seo description="Detailed guide for setting up the DHT sensor with ESPHome" image="dht-sensor.jpg" >}}
Parameters:
- description (optional) - Custom meta description for the page (falls back to page summary if not provided)
- image (optional) - Image to use for social media sharing (will be looked up in the images directory)
Notes:
- This adds Open Graph and Twitter Card metadata for better social media sharing
- The page title is automatically used from the page's front matter
*/}}
{{ $description := .Get "description" }}
{{ $image := .Get "image" }}
{{ $seo := .Page.Param "seo" }}
{{ with $seo }}
{{ $description = $seo.description | default $description }}
{{ $image = $seo.image | default $image }}
{{ end }}
{{ with $description | default .Page.Title }}
<meta name="description" content="{{ . }}">
{{ end }}
{{ with $image }}
<meta property="og:image" content="{{ printf "images/%s" . | absURL }}">
<meta name="twitter:image" content="{{ printf "images/%s" . | absURL }}">
{{ end }}
<meta property="og:title" content="{{ .Page.Title }}">
<meta property="og:description" content="{{ with $description }}{{ . }}{{ else }}{{ .Page.Summary }}{{ end }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ .Page.Permalink }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Page.Title }}">
<meta name="twitter:description" content="{{ with $description }}{{ . }}{{ else }}{{ .Page.Summary }}{{ end }}">

View File

@ -0,0 +1,17 @@
{{/*
TIP SHORTCODE
Creates a tip admonition box to highlight helpful advice or best practices.
Usage:
{{< tip >}}
For best results, place the sensor away from heat sources.
You can include **Markdown** formatting within the tip.
{{< /tip >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the tip box.
*/}}
<div class="admonition tip">
<p class="admonition-title">Tip</p>
{{- .Inner | .Page.RenderString -}}
</div>

View File

@ -0,0 +1,18 @@
{{/*
WARNING SHORTCODE
Creates a warning admonition box to highlight important cautions or potential issues.
Usage:
{{< warning >}}
Incorrect wiring may damage your device. Double-check connections before powering on.
You can include **Markdown** formatting within the warning.
{{< /warning >}}
Content:
The content between the opening and closing shortcode tags will be displayed inside the warning box.
*/}}
<div class="admonition warning">
<p class="admonition-title">Warning</p>
{{ .Inner | .Page.RenderString}}
</div>

View File

@ -0,0 +1,13 @@
name: ESPHome Theme
license: MIT
licenselink: https://github.com/esphome/esphome-docs/LICENSE.md
description: Custom theme for ESPHome documentation
homepage: https://esphome.io/
tags:
- documentation
- responsive
features:
- responsive
- search
- syntax highlighting
min_version: 0.80.0