← back to Norma
feat(theme): hoist InboxThemeBanner + AgeThemeSlider into app/layout.tsx
76b8e92978333a9df5dff55916713be6d25e0ee4 · 2026-05-21 18:45:51 -0700 · Steve Abrams
Both controls now render on every page including /login, /signup, /pulse —
not just inside the authenticated AppShell. Matches the original intent
('set theme and age at the start' from earlier in the session).
- TopThemeBar.tsx: thin client wrapper bundling both components
- layout.tsx (server component): renders <TopThemeBar /> before children
- AppShell.tsx: removed duplicate banner + slider renders
- Both still write to <body data-…>, so behavior is unchanged for
authenticated users; new behavior is that pre-auth visitors can also
set both. Persisted in localStorage.
Files touched
M app/layout.tsxM components/AppShell.tsxA components/TopThemeBar.tsx
Diff
commit 76b8e92978333a9df5dff55916713be6d25e0ee4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 21 18:45:51 2026 -0700
feat(theme): hoist InboxThemeBanner + AgeThemeSlider into app/layout.tsx
Both controls now render on every page including /login, /signup, /pulse —
not just inside the authenticated AppShell. Matches the original intent
('set theme and age at the start' from earlier in the session).
- TopThemeBar.tsx: thin client wrapper bundling both components
- layout.tsx (server component): renders <TopThemeBar /> before children
- AppShell.tsx: removed duplicate banner + slider renders
- Both still write to <body data-…>, so behavior is unchanged for
authenticated users; new behavior is that pre-auth visitors can also
set both. Persisted in localStorage.
---
app/layout.tsx | 4 ++++
components/AppShell.tsx | 11 ++++-------
components/TopThemeBar.tsx | 21 +++++++++++++++++++++
3 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/app/layout.tsx b/app/layout.tsx
index a1e5b5a..3fa31df 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import './globals.css';
+import TopThemeBar from '@/components/TopThemeBar';
export const metadata: Metadata = {
title: 'Norma — The Non-Profit Engine',
@@ -22,6 +23,9 @@ export default function RootLayout({
/>
</head>
<body style={{ fontSize: '16px' }}>
+ {/* Top-of-every-page theme + age controls — render before children
+ so /login etc. inherit the chosen theme on first paint. */}
+ <TopThemeBar />
<div id="app-root">{children}</div>
</body>
</html>
diff --git a/components/AppShell.tsx b/components/AppShell.tsx
index e703dbd..88160f8 100644
--- a/components/AppShell.tsx
+++ b/components/AppShell.tsx
@@ -79,9 +79,9 @@ import EmailSendsTab from './email-sends/EmailSendsTab';
import EmailAnalyzer from './email-analyzer/EmailAnalyzer';
import SavedAnalyses from './email-analyzer/SavedAnalyses';
import GmailCRMTab from './gmail/GmailCRMTab';
-import AgeThemeSlider from './AgeThemeSlider';
+// AgeThemeSlider + InboxThemeBanner imports removed — now live in app/layout.tsx
+// via <TopThemeBar />. AppShell only carries authenticated-only widgets.
import ApiRegistryButton from './api-registry/ApiRegistryButton';
-import InboxThemeBanner from './InboxThemeBanner';
import ImpersonateMenu from './ImpersonateMenu';
import PriceTrackerTab from './price-tracker/PriceTrackerTab';
import SocialTab from './social/SocialTab';
@@ -518,11 +518,8 @@ function Shell() {
{/* ── Impersonation banner (shows ONLY while impersonating) ──────── */}
<ImpersonateMenu variant="banner" />
- {/* ── Gmail CRM theme banner (very top — prev/next/random/dropdown) ─ */}
- <InboxThemeBanner />
-
- {/* ── Age-Adaptive Theme Slider ──────────────────────────────────── */}
- <AgeThemeSlider />
+ {/* Theme banner + age slider moved to app/layout.tsx so they render
+ on every page including /login (set both before authenticating). */}
{/* ── Top Header Bar ─────────────────────────────────────────────── */}
<header
diff --git a/components/TopThemeBar.tsx b/components/TopThemeBar.tsx
new file mode 100644
index 0000000..60bf356
--- /dev/null
+++ b/components/TopThemeBar.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+// Client wrapper for the two top-of-page controls. Lives in app/layout.tsx so
+// they render on EVERY page — including /login, /signup, /pulse, and the
+// authenticated AppShell. Previously they only rendered after auth, which
+// contradicted the "set theme and age at the start" intent.
+//
+// Both controls write to <body data-…>, so the CSS skins + age bands take
+// effect immediately on hydration (no layout shift).
+
+import InboxThemeBanner from './InboxThemeBanner';
+import AgeThemeSlider from './AgeThemeSlider';
+
+export default function TopThemeBar() {
+ return (
+ <>
+ <InboxThemeBanner />
+ <AgeThemeSlider />
+ </>
+ );
+}
← 59324fb docs(sessions): log brute-force-protection learnings (commit
·
back to Norma
·
defensive: explicit body[data-inbox-theme='classic-dense'] s 44cdbed →