'use client'; import * as React from 'react'; import type { ErrorInfo } from 'react'; import { TooltipProvider } from '../components/overlay/tooltip'; import { Toaster } from '../components/feedback/sonner'; import { Boundary, type BoundaryProps } from '../components/boundary'; import { DialogProvider } from '../lib/dialog-service/DialogProvider'; export interface UiProvidersProps { children: React.ReactNode; /** Tooltip open delay in ms. @default 100 (snappy for desktop apps) */ tooltipDelay?: number; /** Disable the Sonner toaster (e.g. when the host renders its own). */ noToaster?: boolean; /** Disable the imperative `window.dialog` service + its renderer. */ noDialogService?: boolean; /** * Top-level crash protection. By default UiProviders wraps the whole tree in * a fullscreen `` so any uncaught React error shows a recoverable * fallback instead of a white screen. Pass `onError` to forward the crash to * your logger / monitor (the host owns logging; the boundary just calls it). * Set `errorBoundary={false}` if the host installs its own top-level boundary. */ onError?: (error: Error, info: ErrorInfo) => void; /** Disable the built-in top-level error boundary. @default false (boundary on) */ errorBoundary?: false; /** * Custom fallback for the built-in boundary. Receives `{ error, errorInfo, * reset }`. Use it to render an app-specific (e.g. i18n'd) crash screen * instead of the default fullscreen one — lets hosts keep a single boundary * (this one) rather than wrapping their own on top. */ errorFallback?: BoundaryProps['fallback']; /** * SSR-safe mount strategy. Default `true` — skips providers on the * initial server render and remounts after `useEffect` so Radix * `` (which calls `useId()` + opens internal state * on mount) doesn't trigger hydration mismatches under Next.js. * * Set `ssr={false}` on CSR-only hosts (Wails, Vite SPA, Storybook * iframe) to skip the deferred mount — providers render on the * very first paint and library components see their context * immediately. */ ssr?: boolean; } /** * One root composition for every overlay/imperative-service the * djangocfg UI library needs: * * - `` — single context root for every `` * in the tree (Radix). Without one, library components log * "Tooltip must be used within TooltipProvider". * - `` — installs the `window.dialog.*` imperative * API and renders the active dialog into a portal. * - `` — Sonner toast portal. Library callsites use * `toast(...)` from the same package; no portal = silent no-op. * * Apple-style: app/host mounts ONE `` at the very top of * the tree, and never again. Library components must NOT include their * own nested `TooltipProvider` — a second context root creates a fresh * provider scope with different delays, and (worse) under Vite dev a * dup-module load yields two `createContext()` instances, breaking the * provider/consumer link. * * Usage: * import { UiProviders } from '@djangocfg/ui-core/lib/providers'; * * * * */ export function UiProviders({ children, tooltipDelay = 100, noToaster, noDialogService, onError, errorBoundary, errorFallback, }: UiProvidersProps) { // No SSR-skip on purpose: any nested library component that renders // `` on its first paint expects to find `` // already in the tree. Skipping the provider during SSR caused // "Tooltip must be used within TooltipProvider" before hydration. // Radix's own provider tolerates the SSR pass — no hydration // mismatches observed; the safe wrapper was over-engineering. const dialogTree = noDialogService ? children : {children}; // Top-level crash protection by default. The Boundary catches uncaught React // errors and shows a recoverable fullscreen fallback; `onError` forwards the // crash to the host's logger/monitor. Opt out with `errorBoundary={false}`. const tree = errorBoundary === false ? ( dialogTree ) : ( {dialogTree} ); return ( {tree} {!noToaster && } ); }