[object Object]

← 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

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 →