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
- 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 ===== */
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{
padding-bottom: 56px;
padding-bottom:56px;
}
.site-container{
max-width: 1100px;
margin: 0 auto;
padding: 20px 18px 0 18px;
max-width:1100px;
margin:0 auto;
padding:20px 18px 0 18px;
}
.site-header{
width: 100%;
width:100%;
}
.site-nav{
@ -34,10 +54,10 @@ body{
width:auto;
display:block;
object-fit:contain;
background: rgba(255,255,255,0.92);
padding: 6px 12px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
background:rgba(255,255,255,0.92);
padding:6px 12px;
border-radius:999px;
box-shadow:0 8px 24px rgba(0,0,0,0.35);
}
.site-title{
@ -48,15 +68,23 @@ body{
.site-title strong{
letter-spacing:.2px;
color: var(--text, #e8eefc);
color:var(--otb-text);
}
.site-title span{
color: var(--muted, #aab6d6);
color:var(--otb-muted);
font-size:13px;
margin-top:2px;
}
.site-nav-right{
display:flex;
align-items:center;
gap:14px;
flex-wrap:wrap;
justify-content:flex-end;
}
.site-navlinks{
display:flex;
gap:12px;
@ -70,15 +98,15 @@ body{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color: var(--muted, #aab6d6);
color:var(--otb-muted);
border:1px solid transparent;
}
.site-navlinks > a:hover,
.dropdown-toggle:hover{
color: var(--text, #e8eefc);
border-color: rgba(255,255,255,.08);
background: rgba(255,255,255,.03);
color:var(--otb-text);
border-color:var(--otb-line);
background:rgba(255,255,255,.03);
}
.dropdown{
@ -99,8 +127,8 @@ body{
display:none;
padding:10px;
border-radius:14px;
background:rgba(18,24,37,.98);
border:1px solid rgba(255,255,255,.08);
background:var(--otb-panel);
border:1px solid var(--otb-line);
box-shadow:0 16px 40px rgba(0,0,0,.35);
z-index:9999;
}
@ -114,7 +142,7 @@ body{
display:block;
padding:9px 10px;
border-radius:10px;
color:var(--muted, #aab6d6);
color:var(--otb-muted);
text-decoration:none;
white-space:nowrap;
margin:0;
@ -125,63 +153,103 @@ body{
}
.dropdown-menu a:hover{
color:var(--text, #e8eefc);
color:var(--otb-text);
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{
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 8px 14px;
background: rgba(8, 16, 32, 0.94);
border-top: 1px solid rgba(255,255,255,.10);
backdrop-filter: blur(8px);
box-shadow: 0 -8px 24px rgba(0,0,0,.28);
position:fixed;
left:0;
right:0;
bottom:0;
z-index:9999;
display:flex;
align-items:center;
justify-content:center;
min-height:42px;
padding:8px 14px;
background:var(--otb-panel);
border-top:1px solid var(--otb-line);
backdrop-filter:blur(8px);
box-shadow:0 -8px 24px rgba(0,0,0,.28);
}
.otb-statusbar-inner{
width: 100%;
max-width: 1100px;
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
text-align: center;
color: var(--muted, #aab6d6);
font-size: 12px;
line-height: 1.35;
width:100%;
max-width:1100px;
display:flex;
gap:10px;
align-items:center;
justify-content:center;
flex-wrap:wrap;
text-align:center;
color:var(--otb-muted);
font-size:12px;
line-height:1.35;
}
.otb-statusbar strong{
color: var(--text, #e8eefc);
font-weight: 700;
color:var(--otb-text);
font-weight:700;
}
.otb-statusbar a{
color: #62e6b7;
text-decoration: none;
font-weight: 600;
color:#62e6b7;
text-decoration:none;
font-weight:600;
}
.otb-statusbar a:hover{
text-decoration: underline;
text-decoration:underline;
}
.otb-dot{
width: 6px;
height: 6px;
border-radius: 999px;
display: inline-block;
background: rgba(255,255,255,.25);
flex: 0 0 auto;
width:6px;
height:6px;
border-radius:999px;
display:inline-block;
background:rgba(255,255,255,.25);
flex:0 0 auto;
}
@media (max-width: 900px){
@ -190,6 +258,11 @@ body{
flex-direction:column;
}
.site-nav-right{
width:100%;
justify-content:space-between;
}
.site-navlinks{
justify-content:flex-start;
}
@ -217,11 +290,11 @@ body{
@media (max-width: 700px){
body{
padding-bottom: 72px;
padding-bottom:72px;
}
.otb-statusbar-inner{
font-size: 11px;
line-height: 1.25;
font-size:11px;
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"/
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)"
python3 - <<PY
cp -av /home/def/otb-shared-brand/brand.js assets/brand.js
python3 - <<PY2
from pathlib import Path
import re
header = """$HEADER"""
footer = """$FOOTER"""
brandcss = """$BRANDCSS"""
header = '''$HEADER'''
footer = '''$FOOTER'''
brandcss = '''$BRANDCSS'''
for name in ["index.html", "pricing.html", "terms.html", "contact.html"]:
p = Path(name)
@ -29,21 +31,21 @@ for name in ["index.html", "pricing.html", "terms.html", "contact.html"]:
elif '<header class="header">' in text:
text = re.sub(r'<header class="header">.*?</header>', header, text, count=1, flags=re.S)
else:
text = text.replace('<body>', '<body>\\n' + header, 1)
text = text.replace('<body>', '<body>\n' + header, 1)
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:
text = text.replace('</body>', footer + '\\n</body>', 1)
text = text.replace('</body>', footer + '\n</body>', 1)
p.write_text(text, encoding="utf-8")
cssp = Path("assets/style.css")
css = cssp.read_text(encoding="utf-8")
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:
css += "\\n\\n" + brandcss
css += "\n\n" + brandcss
cssp.write_text(css, encoding="utf-8")
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
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)"
cp -av /home/def/otb-shared-brand/brand.js frontend/brand.js
cat > frontend/index.html <<HTML
<!doctype html>
<html lang="en" data-theme="dark">
@ -34,11 +36,6 @@ $HEADER
<div class="top-right">
<div class="status-pill" id="status">Loading…</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>
</header>
@ -48,24 +45,22 @@ $HEADER
</div>
$FOOTER
<script src="/app.js" defer></script>
</body>
</html>
HTML
python3 - <<PY
python3 - <<PY2
from pathlib import Path
import re
brandcss = """$BRANDCSS"""
p = Path("frontend/styles.css")
css = p.read_text(encoding="utf-8")
brandcss = '''$BRANDCSS'''
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:
css = brandcss + "\\n\\n" + css
css = brandcss + "\n\n" + css
p.write_text(css, encoding="utf-8")
print("monitor branding css updated")
PY
PY2
/home/def/monitor/deploy-monitor.sh

37
deploy-otb-tracker.sh

@ -1,41 +1,48 @@
#!/bin/bash
set -e
APP_DIR="/opt/otb_tracker"
cd "$APP_DIR" || exit 1
STAMP="$(date +%Y%m%d-%H%M%S)"
BKDIR="backups/shared-brand-$STAMP"
mkdir -p "$BKDIR"
cp -av backend/app.py "$BKDIR"/
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
import re
app = Path("backend/app.py")
text = app.read_text(encoding="utf-8")
header = """$HEADER"""
footer = """$FOOTER"""
header = '''$HEADER'''
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:
text = text.replace("<body>", "<body>\\n" + header)
text = text.replace("<body>", "<body>\n" + header)
# inject footer
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")
print("otb-tracker branding injected")
PY
echo "==== restarting service ===="
sudo systemctl restart otb_tracker.service || true
PY2
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 /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)"
python3 - <<PY
python3 - <<PY2
from pathlib import Path
import re
footer = """$FOOTER"""
brandcss = """$BRANDCSS"""
footer = '''$FOOTER'''
brandcss = '''$BRANDCSS'''
for p in Path("templates").glob("portal_*.html"):
text = p.read_text(encoding="utf-8")
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:
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")
cssp = Path("static/css/style.css")
css = cssp.read_text(encoding="utf-8")
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:
css += "\\n\\n" + brandcss
css += "\n\n" + brandcss
cssp.write_text(css, encoding="utf-8")
print("portal deploy complete")
PY
PY2
sudo systemctl restart otb_billing.service

2
footer.html

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

7
header.html

@ -9,6 +9,7 @@
</div>
</a>
<div class="site-nav-right">
<nav class="site-navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
@ -23,6 +24,12 @@
</div>
<a href="https://otb-billing.outsidethebox.top/portal">Portal</a>
</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>
</header>

Loading…
Cancel
Save