Browse Source

otb-shared-brand v0.3.0: add shared theme toggle and js deploy support

main
def670 1 month ago
parent
commit
e3cedf74a5
  1. 7
      README.md
  2. 2
      VERSION
  3. 191
      brand.css
  4. 24
      brand.js
  5. 24
      deploy-mintme.sh
  6. 21
      deploy-monitor.sh
  7. 37
      deploy-otb-tracker.sh
  8. 19
      deploy-portal.sh
  9. 2
      footer.html
  10. 7
      header.html

7
README.md

@ -1,3 +1,10 @@
## v0.3.0 - 2026-03-22
- Added shared theme toggle markup and behavior via brand.js.
- Updated shared header to include theme toggle.
- Added deploy scripts that also deploy/inject brand.js where needed.
- Updated tracker deploy to inject shared CSS and shared JS.
## v0.2.0 - 2026-03-22 ## v0.2.0 - 2026-03-22
- Added shared brand.css, header.html, and footer.html as the canonical OTB branding source. - Added shared brand.css, header.html, and footer.html as the canonical OTB branding source.

2
VERSION

@ -1 +1 @@
v0.2.0 v0.3.0

191
brand.css

@ -1,16 +1,36 @@
/* ===== OTB shared branding ===== */ /* ===== OTB shared branding ===== */
html[data-theme="dark"]{
--otb-bg:#081225;
--otb-bg2:#09172d;
--otb-text:#e8eefc;
--otb-muted:#aab6d6;
--otb-panel:rgba(18,24,37,.98);
--otb-panel-soft:rgba(18,24,37,.78);
--otb-line:rgba(255,255,255,.08);
}
html[data-theme="light"]{
--otb-bg:#f4f7fb;
--otb-bg2:#eef3f9;
--otb-text:#0f172a;
--otb-muted:#475569;
--otb-panel:rgba(255,255,255,.98);
--otb-panel-soft:rgba(255,255,255,.88);
--otb-line:rgba(15,23,42,.10);
}
body{ body{
padding-bottom: 56px; padding-bottom:56px;
} }
.site-container{ .site-container{
max-width: 1100px; max-width:1100px;
margin: 0 auto; margin:0 auto;
padding: 20px 18px 0 18px; padding:20px 18px 0 18px;
} }
.site-header{ .site-header{
width: 100%; width:100%;
} }
.site-nav{ .site-nav{
@ -34,10 +54,10 @@ body{
width:auto; width:auto;
display:block; display:block;
object-fit:contain; object-fit:contain;
background: rgba(255,255,255,0.92); background:rgba(255,255,255,0.92);
padding: 6px 12px; padding:6px 12px;
border-radius: 999px; border-radius:999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35); box-shadow:0 8px 24px rgba(0,0,0,0.35);
} }
.site-title{ .site-title{
@ -48,15 +68,23 @@ body{
.site-title strong{ .site-title strong{
letter-spacing:.2px; letter-spacing:.2px;
color: var(--text, #e8eefc); color:var(--otb-text);
} }
.site-title span{ .site-title span{
color: var(--muted, #aab6d6); color:var(--otb-muted);
font-size:13px; font-size:13px;
margin-top:2px; margin-top:2px;
} }
.site-nav-right{
display:flex;
align-items:center;
gap:14px;
flex-wrap:wrap;
justify-content:flex-end;
}
.site-navlinks{ .site-navlinks{
display:flex; display:flex;
gap:12px; gap:12px;
@ -70,15 +98,15 @@ body{
text-decoration:none; text-decoration:none;
padding:8px 10px; padding:8px 10px;
border-radius:12px; border-radius:12px;
color: var(--muted, #aab6d6); color:var(--otb-muted);
border:1px solid transparent; border:1px solid transparent;
} }
.site-navlinks > a:hover, .site-navlinks > a:hover,
.dropdown-toggle:hover{ .dropdown-toggle:hover{
color: var(--text, #e8eefc); color:var(--otb-text);
border-color: rgba(255,255,255,.08); border-color:var(--otb-line);
background: rgba(255,255,255,.03); background:rgba(255,255,255,.03);
} }
.dropdown{ .dropdown{
@ -99,8 +127,8 @@ body{
display:none; display:none;
padding:10px; padding:10px;
border-radius:14px; border-radius:14px;
background:rgba(18,24,37,.98); background:var(--otb-panel);
border:1px solid rgba(255,255,255,.08); border:1px solid var(--otb-line);
box-shadow:0 16px 40px rgba(0,0,0,.35); box-shadow:0 16px 40px rgba(0,0,0,.35);
z-index:9999; z-index:9999;
} }
@ -114,7 +142,7 @@ body{
display:block; display:block;
padding:9px 10px; padding:9px 10px;
border-radius:10px; border-radius:10px;
color:var(--muted, #aab6d6); color:var(--otb-muted);
text-decoration:none; text-decoration:none;
white-space:nowrap; white-space:nowrap;
margin:0; margin:0;
@ -125,63 +153,103 @@ body{
} }
.dropdown-menu a:hover{ .dropdown-menu a:hover{
color:var(--text, #e8eefc); color:var(--otb-text);
background:rgba(255,255,255,.04); background:rgba(255,255,255,.04);
} }
.otb-theme-switch{
position:relative;
display:inline-block;
width:54px;
height:30px;
flex:0 0 auto;
}
.otb-theme-switch input{
opacity:0;
width:0;
height:0;
}
.otb-theme-slider{
position:absolute;
inset:0;
cursor:pointer;
background:rgba(255,255,255,.10);
border:1px solid var(--otb-line);
transition:.2s;
border-radius:999px;
}
.otb-theme-slider:before{
content:"";
position:absolute;
height:22px;
width:22px;
left:3px;
top:3px;
background:var(--otb-text);
transition:.2s;
border-radius:50%;
}
.otb-theme-switch input:checked + .otb-theme-slider:before{
transform:translateX(24px);
}
.otb-statusbar{ .otb-statusbar{
position: fixed; position:fixed;
left: 0; left:0;
right: 0; right:0;
bottom: 0; bottom:0;
z-index: 9999; z-index:9999;
display: flex; display:flex;
align-items: center; align-items:center;
justify-content: center; justify-content:center;
min-height: 42px; min-height:42px;
padding: 8px 14px; padding:8px 14px;
background: rgba(8, 16, 32, 0.94); background:var(--otb-panel);
border-top: 1px solid rgba(255,255,255,.10); border-top:1px solid var(--otb-line);
backdrop-filter: blur(8px); backdrop-filter:blur(8px);
box-shadow: 0 -8px 24px rgba(0,0,0,.28); box-shadow:0 -8px 24px rgba(0,0,0,.28);
} }
.otb-statusbar-inner{ .otb-statusbar-inner{
width: 100%; width:100%;
max-width: 1100px; max-width:1100px;
display: flex; display:flex;
gap: 10px; gap:10px;
align-items: center; align-items:center;
justify-content: center; justify-content:center;
flex-wrap: wrap; flex-wrap:wrap;
text-align: center; text-align:center;
color: var(--muted, #aab6d6); color:var(--otb-muted);
font-size: 12px; font-size:12px;
line-height: 1.35; line-height:1.35;
} }
.otb-statusbar strong{ .otb-statusbar strong{
color: var(--text, #e8eefc); color:var(--otb-text);
font-weight: 700; font-weight:700;
} }
.otb-statusbar a{ .otb-statusbar a{
color: #62e6b7; color:#62e6b7;
text-decoration: none; text-decoration:none;
font-weight: 600; font-weight:600;
} }
.otb-statusbar a:hover{ .otb-statusbar a:hover{
text-decoration: underline; text-decoration:underline;
} }
.otb-dot{ .otb-dot{
width: 6px; width:6px;
height: 6px; height:6px;
border-radius: 999px; border-radius:999px;
display: inline-block; display:inline-block;
background: rgba(255,255,255,.25); background:rgba(255,255,255,.25);
flex: 0 0 auto; flex:0 0 auto;
} }
@media (max-width: 900px){ @media (max-width: 900px){
@ -190,6 +258,11 @@ body{
flex-direction:column; flex-direction:column;
} }
.site-nav-right{
width:100%;
justify-content:space-between;
}
.site-navlinks{ .site-navlinks{
justify-content:flex-start; justify-content:flex-start;
} }
@ -217,11 +290,11 @@ body{
@media (max-width: 700px){ @media (max-width: 700px){
body{ body{
padding-bottom: 72px; padding-bottom:72px;
} }
.otb-statusbar-inner{ .otb-statusbar-inner{
font-size: 11px; font-size:11px;
line-height: 1.25; line-height:1.25;
} }
} }

24
brand.js

@ -0,0 +1,24 @@
(function () {
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
const toggle = document.getElementById("otbThemeToggle");
if (toggle) toggle.checked = theme === "light";
}
function savedTheme() {
return localStorage.getItem("otb_theme") || "dark";
}
window.addEventListener("DOMContentLoaded", function () {
applyTheme(savedTheme());
const toggle = document.getElementById("otbThemeToggle");
if (!toggle) return;
toggle.addEventListener("change", function () {
const theme = toggle.checked ? "light" : "dark";
localStorage.setItem("otb_theme", theme);
applyTheme(theme);
});
});
})();

24
deploy-mintme.sh

@ -9,16 +9,18 @@ mkdir -p "$BKDIR"
cp -av index.html pricing.html terms.html contact.html assets/style.css "$BKDIR"/ cp -av index.html pricing.html terms.html contact.html assets/style.css "$BKDIR"/
HEADER="$(cat /home/def/otb-shared-brand/header.html)" HEADER="$(cat /home/def/otb-shared-brand/header.html)"
FOOTER="$(cat /home/def/otb-shared-brand/footer.html)" FOOTER="$(cat /home/def/otb-shared-brand/footer.html | sed 's|__OTB_BRAND_JS__|/assets/brand.js|g')"
BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)" BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)"
python3 - <<PY cp -av /home/def/otb-shared-brand/brand.js assets/brand.js
python3 - <<PY2
from pathlib import Path from pathlib import Path
import re import re
header = """$HEADER""" header = '''$HEADER'''
footer = """$FOOTER""" footer = '''$FOOTER'''
brandcss = """$BRANDCSS""" brandcss = '''$BRANDCSS'''
for name in ["index.html", "pricing.html", "terms.html", "contact.html"]: for name in ["index.html", "pricing.html", "terms.html", "contact.html"]:
p = Path(name) p = Path(name)
@ -29,21 +31,21 @@ for name in ["index.html", "pricing.html", "terms.html", "contact.html"]:
elif '<header class="header">' in text: elif '<header class="header">' in text:
text = re.sub(r'<header class="header">.*?</header>', header, text, count=1, flags=re.S) text = re.sub(r'<header class="header">.*?</header>', header, text, count=1, flags=re.S)
else: else:
text = text.replace('<body>', '<body>\\n' + header, 1) text = text.replace('<body>', '<body>\n' + header, 1)
if '<div class="otb-statusbar">' in text: if '<div class="otb-statusbar">' in text:
text = re.sub(r'<div class="otb-statusbar">.*?</div>\\s*</div>', footer, text, count=1, flags=re.S) text = re.sub(r'<div class="otb-statusbar">.*?</script>', footer, text, count=1, flags=re.S)
else: else:
text = text.replace('</body>', footer + '\\n</body>', 1) text = text.replace('</body>', footer + '\n</body>', 1)
p.write_text(text, encoding="utf-8") p.write_text(text, encoding="utf-8")
cssp = Path("assets/style.css") cssp = Path("assets/style.css")
css = cssp.read_text(encoding="utf-8") css = cssp.read_text(encoding="utf-8")
if "/* ===== OTB shared branding ===== */" in css: if "/* ===== OTB shared branding ===== */" in css:
css = re.sub(r'/\\* ===== OTB shared branding ===== \\*/.*', brandcss, css, flags=re.S) css = re.sub(r'/\* ===== OTB shared branding ===== \*/.*', brandcss, css, flags=re.S)
else: else:
css += "\\n\\n" + brandcss css += "\n\n" + brandcss
cssp.write_text(css, encoding="utf-8") cssp.write_text(css, encoding="utf-8")
print("mintme deploy complete") print("mintme deploy complete")
PY PY2

21
deploy-monitor.sh

@ -9,9 +9,11 @@ mkdir -p "$BKDIR"
cp -av frontend/index.html frontend/styles.css "$BKDIR"/ || true cp -av frontend/index.html frontend/styles.css "$BKDIR"/ || true
HEADER="$(cat /home/def/otb-shared-brand/header.html)" HEADER="$(cat /home/def/otb-shared-brand/header.html)"
FOOTER="$(cat /home/def/otb-shared-brand/footer.html)" FOOTER="$(cat /home/def/otb-shared-brand/footer.html | sed 's|__OTB_BRAND_JS__|/brand.js|g')"
BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)" BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)"
cp -av /home/def/otb-shared-brand/brand.js frontend/brand.js
cat > frontend/index.html <<HTML cat > frontend/index.html <<HTML
<!doctype html> <!doctype html>
<html lang="en" data-theme="dark"> <html lang="en" data-theme="dark">
@ -34,11 +36,6 @@ $HEADER
<div class="top-right"> <div class="top-right">
<div class="status-pill" id="status">Loading…</div> <div class="status-pill" id="status">Loading…</div>
<div class="cycle" id="cycle"></div> <div class="cycle" id="cycle"></div>
<label class="switch" title="Toggle theme">
<input type="checkbox" id="themeToggle" aria-label="Toggle theme" />
<span class="slider"></span>
</label>
</div> </div>
</header> </header>
@ -48,24 +45,22 @@ $HEADER
</div> </div>
$FOOTER $FOOTER
<script src="/app.js" defer></script>
</body> </body>
</html> </html>
HTML HTML
python3 - <<PY python3 - <<PY2
from pathlib import Path from pathlib import Path
import re import re
brandcss = """$BRANDCSS"""
p = Path("frontend/styles.css") p = Path("frontend/styles.css")
css = p.read_text(encoding="utf-8") css = p.read_text(encoding="utf-8")
brandcss = '''$BRANDCSS'''
if "/* ===== OTB shared branding ===== */" in css: if "/* ===== OTB shared branding ===== */" in css:
css = re.sub(r'/\\* ===== OTB shared branding ===== \\*/.*', brandcss, css, flags=re.S) css = re.sub(r'/\* ===== OTB shared branding ===== \*/.*', brandcss, css, flags=re.S)
else: else:
css = brandcss + "\\n\\n" + css css = brandcss + "\n\n" + css
p.write_text(css, encoding="utf-8") p.write_text(css, encoding="utf-8")
print("monitor branding css updated") print("monitor branding css updated")
PY PY2
/home/def/monitor/deploy-monitor.sh /home/def/monitor/deploy-monitor.sh

37
deploy-otb-tracker.sh

@ -1,41 +1,48 @@
#!/bin/bash #!/bin/bash
set -e set -e
APP_DIR="/opt/otb_tracker" APP_DIR="/opt/otb_tracker"
cd "$APP_DIR" || exit 1 cd "$APP_DIR" || exit 1
STAMP="$(date +%Y%m%d-%H%M%S)" STAMP="$(date +%Y%m%d-%H%M%S)"
BKDIR="backups/shared-brand-$STAMP" BKDIR="backups/shared-brand-$STAMP"
mkdir -p "$BKDIR" mkdir -p "$BKDIR"
cp -av backend/app.py "$BKDIR"/ cp -av backend/app.py "$BKDIR"/
HEADER="$(cat /home/def/otb-shared-brand/header.html)" HEADER="$(cat /home/def/otb-shared-brand/header.html)"
FOOTER="$(cat /home/def/otb-shared-brand/footer.html)" FOOTER="$(cat /home/def/otb-shared-brand/footer.html | sed 's|__OTB_BRAND_JS__|/static/brand.js|g')"
BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)"
BRANDJS="$(cat /home/def/otb-shared-brand/brand.js)"
python3 - <<PY mkdir -p static
printf '%s
' "$BRANDJS" > static/brand.js
python3 - <<PY2
from pathlib import Path from pathlib import Path
import re import re
app = Path("backend/app.py") app = Path("backend/app.py")
text = app.read_text(encoding="utf-8") text = app.read_text(encoding="utf-8")
header = """$HEADER""" header = '''$HEADER'''
footer = """$FOOTER""" footer = '''$FOOTER'''
brandcss = '''$BRANDCSS'''
style_block = f"<style>\n{brandcss}\n</style>"
text = re.sub(r'<style>\s*/\* ===== OTB shared branding ===== \*/.*?</style>', '', text, flags=re.S)
# inject header
if header not in text: if header not in text:
text = text.replace("<body>", "<body>\\n" + header) text = text.replace("<body>", "<body>\n" + header)
# inject footer
if footer not in text: if footer not in text:
text = text.replace("</body>", footer + "\\n</body>") text = text.replace("</body>", footer + "\n</body>")
if style_block not in text:
text = text.replace("</head>", style_block + "\n</head>")
app.write_text(text, encoding="utf-8") app.write_text(text, encoding="utf-8")
print("otb-tracker branding injected") print("otb-tracker branding injected")
PY PY2
echo "==== restarting service ===="
sudo systemctl restart otb_tracker.service || true
echo "==== done ====" sudo systemctl restart otb-tracker.service || true

19
deploy-portal.sh

@ -9,33 +9,34 @@ mkdir -p "$BKDIR"
cp -av templates/includes/site_nav.html static/css/style.css templates/portal_*.html "$BKDIR"/ cp -av templates/includes/site_nav.html static/css/style.css templates/portal_*.html "$BKDIR"/
cp -av /home/def/otb-shared-brand/header.html templates/includes/site_nav.html cp -av /home/def/otb-shared-brand/header.html templates/includes/site_nav.html
cp -av /home/def/otb-shared-brand/brand.js static/brand.js
FOOTER="$(cat /home/def/otb-shared-brand/footer.html)" FOOTER="$(cat /home/def/otb-shared-brand/footer.html | sed 's|__OTB_BRAND_JS__|/static/brand.js|g')"
BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)" BRANDCSS="$(cat /home/def/otb-shared-brand/brand.css)"
python3 - <<PY python3 - <<PY2
from pathlib import Path from pathlib import Path
import re import re
footer = """$FOOTER""" footer = '''$FOOTER'''
brandcss = """$BRANDCSS""" brandcss = '''$BRANDCSS'''
for p in Path("templates").glob("portal_*.html"): for p in Path("templates").glob("portal_*.html"):
text = p.read_text(encoding="utf-8") text = p.read_text(encoding="utf-8")
if '<div class="otb-statusbar">' in text: if '<div class="otb-statusbar">' in text:
text = re.sub(r'<div class="otb-statusbar">.*?</div>\\s*</div>', footer, text, count=1, flags=re.S) text = re.sub(r'<div class="otb-statusbar">.*?</script>', footer, text, count=1, flags=re.S)
else: else:
text = text.replace('{% include "footer.html" %}', footer + '\\n\\n {% include "footer.html" %}') text = text.replace('{% include "footer.html" %}', footer + '\n\n {% include "footer.html" %}')
p.write_text(text, encoding="utf-8") p.write_text(text, encoding="utf-8")
cssp = Path("static/css/style.css") cssp = Path("static/css/style.css")
css = cssp.read_text(encoding="utf-8") css = cssp.read_text(encoding="utf-8")
if "/* ===== OTB shared branding ===== */" in css: if "/* ===== OTB shared branding ===== */" in css:
css = re.sub(r'/\\* ===== OTB shared branding ===== \\*/.*', brandcss, css, flags=re.S) css = re.sub(r'/\* ===== OTB shared branding ===== \*/.*', brandcss, css, flags=re.S)
else: else:
css += "\\n\\n" + brandcss css += "\n\n" + brandcss
cssp.write_text(css, encoding="utf-8") cssp.write_text(css, encoding="utf-8")
print("portal deploy complete") print("portal deploy complete")
PY PY2
sudo systemctl restart otb_billing.service sudo systemctl restart otb_billing.service

2
footer.html

@ -12,3 +12,5 @@
</span> </span>
</div> </div>
</div> </div>
<script src="__OTB_BRAND_JS__" defer></script>

7
header.html

@ -9,6 +9,7 @@
</div> </div>
</a> </a>
<div class="site-nav-right">
<nav class="site-navlinks"> <nav class="site-navlinks">
<a href="https://outsidethebox.top">Home</a> <a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a> <a href="https://outsidethebox.top/pricing.html">Pricing</a>
@ -23,6 +24,12 @@
</div> </div>
<a href="https://otb-billing.outsidethebox.top/portal">Portal</a> <a href="https://otb-billing.outsidethebox.top/portal">Portal</a>
</nav> </nav>
<label class="otb-theme-switch" title="Toggle light / dark mode">
<input type="checkbox" id="otbThemeToggle" aria-label="Toggle theme" />
<span class="otb-theme-slider"></span>
</label>
</div>
</div> </div>
</div> </div>
</header> </header>

Loading…
Cancel
Save