Files
eranos/src/components/SubHeaderBarContext.ts
T
Chad Curtis 17c1936817 Support follow pack/set naddr identifiers on /follow URL
The /follow route now accepts naddr1 identifiers for follow packs
(kind 39089) and follow sets (kind 30000) in addition to npub/nprofile.

Renders an immersive fullscreen layout with pack info hero, avatar
stack, big Follow All CTA with status indicator, and Feed/Members
tabs using the standard SubHeaderBar arc.

Follow All uses the safe fetch-fresh -> modify -> publish pattern
with prev for published_at preservation.

Shared components (PackFeedTab, MemberCard, MemberCardSkeleton) and
parsePackEvent are reused from FollowPackDetailContent and packUtils.

Also fixes SubHeaderBar tab indicator positioning when innerClassName
centers the tab container (adds containerOffset + ResizeObserver for
layout-dependent recalculation).
2026-04-07 08:55:27 -05:00

84 lines
2.9 KiB
TypeScript

import { createContext, useContext, useCallback, useLayoutEffect, useEffect } from 'react';
interface HoverSlice {
left: number;
width: number;
}
interface SubHeaderBarContextValue {
onHover: (slice: HoverSlice | null) => void;
onActive: (slice: HoverSlice | null) => void;
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
}
export const SubHeaderBarContext = createContext<SubHeaderBarContextValue>({ onHover: () => {}, onActive: () => {}, scrollContainerRef: { current: null } });
export function useSubHeaderBarHover() {
return useContext(SubHeaderBarContext);
}
/**
* Shared hook for reporting the active tab's position to SubHeaderBar's arc indicator.
* Handles scroll-aware position reporting and cleans up on unmount/deactivation.
*
* @param active Whether this tab is currently active.
* @param elRef Ref to the tab's DOM element (used for offsetLeft/offsetWidth).
*/
export function useActiveTabIndicator(active: boolean, elRef: React.RefObject<HTMLElement | null>) {
const { onActive, scrollContainerRef } = useSubHeaderBarHover();
const reportSlice = useCallback(() => {
const el = elRef.current;
if (!el) return null;
const container = scrollContainerRef.current;
const scrollOffset = container?.scrollLeft ?? 0;
// Account for the scroll container's own offset within its parent
// (e.g. when innerClassName adds mx-auto centering).
const containerOffset = container?.offsetLeft ?? 0;
return { left: el.offsetLeft - scrollOffset + containerOffset, width: el.offsetWidth };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Report active slice to SubHeaderBar so the arc indicator renders.
// Schedule a second report after paint so that layout-dependent values
// (e.g. offsetLeft from mx-auto centering) are fully resolved.
useLayoutEffect(() => {
if (!active) return;
const s = reportSlice();
if (s) onActive(s);
const raf = requestAnimationFrame(() => {
const updated = reportSlice();
if (updated) onActive(updated);
});
return () => {
cancelAnimationFrame(raf);
onActive(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active]);
// Re-report position when the scroll container scrolls or resizes,
// so the SVG clip-path stays aligned with the visually shifted tab.
useEffect(() => {
if (!active) return;
const container = scrollContainerRef.current;
if (!container) return;
const update = () => {
const s = reportSlice();
if (s) onActive(s);
};
container.addEventListener('scroll', update, { passive: true });
const ro = new ResizeObserver(update);
ro.observe(container);
return () => {
container.removeEventListener('scroll', update);
ro.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active]);
return { reportSlice };
}