The browser-fetching variant ran into the cross-origin / cert-trust wall that came up the moment a real user loaded the page. Move the network call out of the library: <AppShell> now takes a pre-resolved ShellConfig as a required prop. Apps fetch from gsc-shell-api in their RSC layout (server-to-server, no CORS, no cert dance) and pass the result down. Public surface: AppShell, useShell, ShellConfig + sub-types Removed: ShellProvider (network logic gone), all fetcher props (apiUrl, getToken, appKey, initialConfig, revalidateMs). useShell() now returns ShellConfig directly (was ShellContextValue with loading/error/refresh — no longer relevant). Apps drive revalidation by re-rendering; chrome stays request-fresh without any client-side polling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useFloating, offset, shift, flip, arrow, Placement, Middleware } from '@floating-ui/react';
|
|
|
|
export type TooltipProps = {
|
|
content: React.ReactNode;
|
|
placement?: Placement;
|
|
children: React.ReactElement<any>;
|
|
className?: string;
|
|
};
|
|
|
|
/**
|
|
* Minimal tooltip using floating-ui.
|
|
*/
|
|
export function Tooltip({ content, placement = 'top', children, className = '' }: TooltipProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [arrowEl, setArrowEl] = useState<HTMLElement | null>(null);
|
|
|
|
const middleware: Middleware[] = [offset(8), flip(), shift()];
|
|
if (arrowEl) middleware.push(arrow({ element: arrowEl }));
|
|
|
|
const { x, y, refs, strategy, middlewareData, placement: finalPlacement } = useFloating({
|
|
open,
|
|
onOpenChange: setOpen,
|
|
placement,
|
|
middleware
|
|
});
|
|
|
|
const staticSide = {
|
|
top: 'bottom',
|
|
right: 'left',
|
|
bottom: 'top',
|
|
left: 'right'
|
|
}[finalPlacement.split('-')[0]] as string;
|
|
|
|
return (
|
|
<>
|
|
{React.cloneElement(children, {
|
|
ref: refs.setReference,
|
|
onMouseEnter: () => setOpen(true),
|
|
onMouseLeave: () => setOpen(false),
|
|
onFocus: () => setOpen(true),
|
|
onBlur: () => setOpen(false)
|
|
})}
|
|
{open ? (
|
|
<div
|
|
ref={refs.setFloating}
|
|
className={['tooltip bs-tooltip-auto show', className].filter(Boolean).join(' ')}
|
|
role="tooltip"
|
|
style={{
|
|
position: strategy,
|
|
top: y ?? 0,
|
|
left: x ?? 0
|
|
}}
|
|
>
|
|
<div className="tooltip-inner">{content}</div>
|
|
<div
|
|
ref={setArrowEl as any}
|
|
className="tooltip-arrow"
|
|
style={{
|
|
left: middlewareData.arrow?.x,
|
|
top: middlewareData.arrow?.y,
|
|
[staticSide]: '-4px'
|
|
}}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|