WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-54-add-lightdark-mode-toggle 139 lines 4.8 kB view raw
1import type { FC, PropsWithChildren } from "hono/jsx"; 2import { tokensToCss } from "../lib/theme.js"; 3import { sanitizeCss } from "@atbb/css-sanitizer"; 4import type { ResolvedTheme } from "../lib/theme-resolution.js"; 5import type { WebSession } from "../lib/session.js"; 6 7const NavContent: FC<{ auth?: WebSession; colorScheme: "light" | "dark" }> = ({ 8 auth, 9 colorScheme, 10}) => { 11 const toggleLabel = 12 colorScheme === "light" ? "Switch to dark mode" : "Switch to light mode"; 13 const toggleIcon = colorScheme === "light" ? "\u263D" : "\u2600"; 14 return ( 15 <> 16 <button 17 class="color-scheme-toggle" 18 onclick="toggleColorScheme()" 19 aria-label={toggleLabel} 20 title={toggleLabel} 21 > 22 {toggleIcon} 23 </button> 24 {auth?.authenticated ? ( 25 <> 26 <span class="site-header__handle">{auth.handle}</span> 27 <form action="/logout" method="post" class="site-header__logout-form"> 28 <button type="submit" class="site-header__logout-btn"> 29 Log out 30 </button> 31 </form> 32 </> 33 ) : ( 34 <a href="/login" class="site-header__login-link"> 35 Log in 36 </a> 37 )} 38 </> 39 ); 40}; 41 42export const BaseLayout: FC< 43 PropsWithChildren<{ 44 title?: string; 45 auth?: WebSession; 46 resolvedTheme: ResolvedTheme; 47 }> 48> = (props) => { 49 const { auth, resolvedTheme } = props; 50 51 let rootCss = ""; 52 try { 53 rootCss = sanitizeCss(`:root { ${tokensToCss(resolvedTheme.tokens)} }`); 54 } catch (err) { 55 console.error("Failed to sanitize root CSS tokens — rendering without tokens", { 56 error: String(err), 57 }); 58 } 59 60 let overridesCss: string | null = null; 61 if (resolvedTheme.cssOverrides) { 62 try { 63 overridesCss = sanitizeCss(resolvedTheme.cssOverrides); 64 } catch (err) { 65 console.error("Failed to sanitize CSS overrides — rendering without overrides", { 66 error: String(err), 67 }); 68 } 69 } 70 71 return ( 72 <html lang="en"> 73 <head> 74 <meta charset="UTF-8" /> 75 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 76 <meta http-equiv="Accept-CH" content="Sec-CH-Prefers-Color-Scheme" /> 77 <title>{props.title ?? "atBB Forum"}</title> 78 <style dangerouslySetInnerHTML={{ __html: rootCss }} /> 79 {overridesCss && ( 80 <style dangerouslySetInnerHTML={{ __html: overridesCss }} /> 81 )} 82 {resolvedTheme.fontUrls && resolvedTheme.fontUrls.length > 0 && (() => { 83 const safeFontUrls = resolvedTheme.fontUrls!.filter((url) => url.startsWith("https://")); 84 return safeFontUrls.length > 0 ? ( 85 <> 86 <link rel="preconnect" href="https://fonts.googleapis.com" /> 87 <link 88 rel="preconnect" 89 href="https://fonts.gstatic.com" 90 crossorigin="anonymous" 91 /> 92 {safeFontUrls.map((url) => ( 93 <link rel="stylesheet" href={url} /> 94 ))} 95 </> 96 ) : null; 97 })()} 98 <link rel="stylesheet" href="/static/css/reset.css" /> 99 <link rel="stylesheet" href="/static/css/theme.css" /> 100 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" /> 101 <script src="https://unpkg.com/htmx.org@2.0.4" defer /> 102 </head> 103 <body> 104 <a href="#main-content" class="skip-link"> 105 Skip to main content 106 </a> 107 <header class="site-header"> 108 <div class="site-header__inner"> 109 <a href="/" class="site-header__title"> 110 atBB Forum 111 </a> 112 <nav class="desktop-nav" aria-label="Main navigation"> 113 <NavContent auth={auth} colorScheme={resolvedTheme.colorScheme} /> 114 </nav> 115 <details class="mobile-nav"> 116 <summary class="mobile-nav__toggle" aria-label="Menu"> 117 &#9776; 118 </summary> 119 <nav class="mobile-nav__menu" aria-label="Mobile navigation"> 120 <NavContent auth={auth} colorScheme={resolvedTheme.colorScheme} /> 121 </nav> 122 </details> 123 </div> 124 </header> 125 <main id="main-content" class="content-container"> 126 {props.children} 127 </main> 128 <footer class="site-footer"> 129 <p>Powered by atBB on the ATmosphere</p> 130 </footer> 131 <script 132 dangerouslySetInnerHTML={{ 133 __html: `function toggleColorScheme(){var m=document.cookie.match(/(?:^|;\\s*)atbb-color-scheme=(light|dark)/);var next=m&&m[1]==='light'?'dark':'light';document.cookie='atbb-color-scheme='+next+';path=/;max-age=31536000;SameSite=Lax';location.reload();}`, 134 }} 135 /> 136 </body> 137 </html> 138 ); 139};