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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user