The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh reloads a module that also exports hooks or helpers. - Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts), following navigationInit/playlistsInit. - Helpers that sat in component files have their own modules: welcomeMode, downloadTitle, plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent, ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns and the registry's page wrappers are components files of their own (channelCells, modulePages). - usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original. npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { useState, useSyncExternalStore } from "react";
|
|
import { PopoverSurface } from "./AnchoredPopover";
|
|
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
|
import { hintsEnabled, subscribeHints } from "../../lib/hints";
|
|
|
|
type Side = "top" | "bottom" | "left" | "right";
|
|
|
|
/** Wrap any element to show a short glass hint caption on hover — by default only while the
|
|
* app-wide hints toggle (Settings → Appearance) is on. Anchored by the R8 popover platform, so it
|
|
* is never clipped by an overflow/stacking ancestor, flips when the preferred side runs out of room
|
|
* and slides back inside the viewport near an edge.
|
|
* `always` bypasses the hints toggle (for a label that IS the primary affordance, e.g. the slim nav
|
|
* rail's flyout names); `openDelay` waits before showing (a rest-on-the-icon feel). */
|
|
export default function Tooltip({
|
|
hint,
|
|
side = "top",
|
|
always = false,
|
|
openDelay = 0,
|
|
children,
|
|
}: {
|
|
hint: string;
|
|
side?: Side;
|
|
always?: boolean;
|
|
openDelay?: number;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const hintsOn = useSyncExternalStore(subscribeHints, hintsEnabled, hintsEnabled);
|
|
const enabled = always || hintsOn;
|
|
const [open, setOpen] = useState(false);
|
|
const pop = useAnchoredPopover({
|
|
open: open && enabled && !!hint,
|
|
onOpenChange: setOpen,
|
|
mode: "hover",
|
|
placement: side,
|
|
offset: 8,
|
|
// Show/hide on contact by default (as the pre-platform tooltip did); `openDelay` adds a
|
|
// rest-before-show for labels that shouldn't flash on a fast sweep across an icon row.
|
|
hoverDelay: { open: openDelay, close: 0 },
|
|
});
|
|
|
|
if (!enabled || !hint) return <>{children}</>;
|
|
|
|
return (
|
|
<>
|
|
<span ref={pop.refs.setReference} {...pop.getReferenceProps()} className="inline-flex">
|
|
{children}
|
|
</span>
|
|
{/* Passive caption: it must not swallow a click aimed at a neighbouring control that it
|
|
overlaps while the hover-close delay runs out. */}
|
|
<PopoverSurface
|
|
popover={pop}
|
|
passthrough
|
|
className="glass w-max max-w-[240px] px-2.5 py-1.5 rounded-lg text-xs leading-snug text-fg font-normal normal-case tracking-normal animate-[fadeIn_0.12s_ease]"
|
|
>
|
|
{hint}
|
|
</PopoverSurface>
|
|
</>
|
|
);
|
|
}
|