Simplify profile theme: swap global CSS vars instead of scoped theme wrapper

- Remove ScopedThemeColumns, scopedThemeTokens from LayoutContext and MainLayout
- Profile page now temporarily overrides :root CSS vars with the visited user's theme
- On navigation away, user's own theme is restored via effect cleanup
- Entire page (sidebar, center, right) uses the profile theme
- Removed all display:contents and background extension hacks
This commit is contained in:
Mary Kate Fain
2026-02-25 18:36:31 -06:00
parent 11e779e79b
commit 5fafe9c8a7
4 changed files with 47 additions and 35 deletions
+12 -30
View File
@@ -7,7 +7,6 @@ import { MobileBottomNav } from '@/components/MobileBottomNav';
import { MobileDrawer } from '@/components/MobileDrawer';
import { FloatingComposeButton } from '@/components/FloatingComposeButton';
import { Skeleton } from '@/components/ui/skeleton';
import { ScopedTheme } from '@/components/ScopedTheme';
import { LayoutStore, LayoutStoreContext, useLayoutSnapshot } from '@/contexts/LayoutContext';
import { cn } from '@/lib/utils';
@@ -63,7 +62,7 @@ function PageSkeleton() {
/** Inner component that reads layout options from the context store. */
function MainLayoutInner() {
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, scopedThemeTokens, noBottomSpacer = false, wrapperClassName } = useLayoutSnapshot();
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, noBottomSpacer = false, wrapperClassName } = useLayoutSnapshot();
const [drawerOpen, setDrawerOpen] = useState(false);
return (
@@ -83,20 +82,18 @@ function MainLayoutInner() {
{/* Main content + right sidebar: inside Suspense so the left sidebar persists while lazy pages load */}
<Suspense fallback={<PageSkeleton />}>
<ScopedThemeColumns tokens={scopedThemeTokens}>
{/* Wrap the center column in a relative container for the FAB */}
<div className={cn("relative flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border", showFAB && "pb-24")}>
<Outlet />
{showFAB && (
<div className="sticky bottom-20 sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6">
<div className="pointer-events-auto">
<FloatingComposeButton kind={fabKind} href={fabHref} />
</div>
{/* Wrap the center column in a relative container for the FAB */}
<div className={cn("relative flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border", showFAB && "pb-24")}>
<Outlet />
{showFAB && (
<div className="sticky bottom-20 sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6">
<div className="pointer-events-auto">
<FloatingComposeButton kind={fabKind} href={fabHref} />
</div>
)}
</div>
{rightSidebar ?? <RightSidebar />}
</ScopedThemeColumns>
</div>
)}
</div>
{rightSidebar ?? <RightSidebar />}
</Suspense>
</div>
@@ -109,21 +106,6 @@ function MainLayoutInner() {
);
}
/**
* Wraps center + right columns in a ScopedTheme when profile theme tokens are provided.
* Uses `display: contents` so the wrapper doesn't affect the flex layout.
*/
function ScopedThemeColumns({ tokens, children }: { tokens?: import('@/themes').ThemeTokens; children: React.ReactNode }) {
if (!tokens) return <>{children}</>;
return (
<ScopedTheme tokens={tokens} className="relative flex flex-1 min-w-0 bg-background text-foreground">
{/* Extend background to the right edge of the viewport */}
<div className="absolute top-0 bottom-0 left-0 bg-background pointer-events-none" style={{ right: 'calc(-50vw + 50%)' }} aria-hidden />
{children}
</ScopedTheme>
);
}
/**
* Persistent layout shell rendered once by the router.
* Provides a LayoutStore so child pages can configure layout options
+1 -1
View File
@@ -315,7 +315,7 @@ export function ProfileRightSidebar({ fields, mediaEvents, mediaLoading: mediaLo
const mediaLoading = mediaLoadingProp ?? false;
return (
<aside className="w-[300px] shrink-0 hidden xl:flex flex-col sticky top-0 h-screen overflow-y-auto pt-5 pb-3 px-5 bg-background">
<aside className="w-[300px] shrink-0 hidden xl:flex flex-col sticky top-0 h-screen overflow-y-auto pt-5 pb-3 px-5">
{/* Media Section */}
<section className="mb-6">
<h2 className="text-xl font-bold mb-3">Media</h2>
-3
View File
@@ -10,8 +10,6 @@ export interface LayoutOptions {
fabKind?: number;
/** If set, the FAB navigates to this URL instead of opening a compose dialog. */
fabHref?: string;
/** If set, applies scoped theme CSS variables to the center and right columns. */
scopedThemeTokens?: import('@/themes').ThemeTokens;
/** Skip the bottom nav spacer (for pages that handle their own bottom padding) */
noBottomSpacer?: boolean;
/** Additional classes for the wrapper div */
@@ -76,7 +74,6 @@ export function useLayoutOptions(options: LayoutOptions): void {
prev.current.showFAB !== options.showFAB ||
prev.current.fabKind !== options.fabKind ||
prev.current.fabHref !== options.fabHref ||
prev.current.scopedThemeTokens !== options.scopedThemeTokens ||
prev.current.noBottomSpacer !== options.noBottomSpacer ||
prev.current.wrapperClassName !== options.wrapperClassName ||
prev.current.rightSidebar !== options.rightSidebar;
+34 -1
View File
@@ -38,7 +38,9 @@ import { EmojifiedText } from '@/components/CustomEmoji';
import { PullToRefresh } from '@/components/PullToRefresh';
import { useActiveProfileTheme } from '@/hooks/useActiveProfileTheme';
import { useTheme } from '@/hooks/useTheme';
import { useFeedSettings } from '@/hooks/useFeedSettings';
import { buildThemeCss, builtinThemes, resolveTheme } from '@/themes';
import { cn, STICKY_HEADER_CLASS } from '@/lib/utils';
import type { FeedItem } from '@/lib/feedUtils';
import type { NostrEvent } from '@nostrify/nostrify';
@@ -723,6 +725,38 @@ export function ProfilePage() {
);
const profileThemeTokens = profileThemeQuery.data?.tokens;
// Temporarily apply the visited user's theme globally while on their profile
const { theme: ownTheme, customTheme: ownCustomTheme } = useTheme();
useEffect(() => {
if (!profileThemeTokens) return;
// Inject the profile theme's CSS vars onto :root
const css = buildThemeCss(profileThemeTokens);
let el = document.getElementById('theme-vars') as HTMLStyleElement | null;
if (!el) {
el = document.createElement('style');
el.id = 'theme-vars';
document.head.appendChild(el);
}
const previousCss = el.textContent;
el.textContent = css;
// Restore the user's own theme on cleanup
return () => {
const styleEl = document.getElementById('theme-vars') as HTMLStyleElement | null;
if (styleEl) {
if (previousCss) {
styleEl.textContent = previousCss;
} else {
// Fallback: rebuild from current theme setting
const resolved = resolveTheme(ownTheme);
const tokens = ownCustomTheme ?? builtinThemes[resolved as keyof typeof builtinThemes] ?? builtinThemes.dark;
styleEl.textContent = buildThemeCss(tokens);
}
}
};
}, [profileThemeTokens]); // eslint-disable-line react-hooks/exhaustive-deps
const pinnedIds = useMemo(() => supplementary?.pinnedIds ?? [], [supplementary?.pinnedIds]);
const { data: pinnedEvents = [], isLoading: pinnedEventsLoading } = useQuery({
@@ -883,7 +917,6 @@ export function ProfilePage() {
useLayoutOptions(pubkey ? {
rightSidebar: <ProfileRightSidebar fields={fields} mediaEvents={mediaEvents} mediaLoading={mediaPending} />,
showFAB: true,
scopedThemeTokens: profileThemeTokens,
} : {});
if (!pubkey) {