@djangocfg/ui-core 2.1.415 → 2.1.417

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,32 @@ pnpm add @djangocfg/ui-core
22
22
  import { Button, Card, Sidebar, SSRPagination } from '@djangocfg/ui-core/components';
23
23
  import { useIsMobile, useNavigate, useQueryParams } from '@djangocfg/ui-core/hooks';
24
24
  import { cn } from '@djangocfg/ui-core/lib';
25
+ import { UiProviders } from '@djangocfg/ui-core/providers';
25
26
  ```
26
27
 
28
+ ## Root providers
29
+
30
+ Mount `<UiProviders>` once at the top of your app — it bundles the overlay
31
+ and imperative services every ui-core / ui-tools component expects:
32
+
33
+ ```tsx
34
+ import { UiProviders } from '@djangocfg/ui-core/providers';
35
+
36
+ export function Root({ children }: { children: React.ReactNode }) {
37
+ return <UiProviders>{children}</UiProviders>;
38
+ }
39
+ ```
40
+
41
+ Includes:
42
+ - `<TooltipProvider>` (Radix tooltip root, `delayDuration={100}` by default)
43
+ - `<DialogProvider>` (installs `window.dialog.*` + renders active dialogs)
44
+ - `<Toaster>` (Sonner toast portal)
45
+
46
+ Opt out per-service: `<UiProviders noToaster noDialogService>`.
47
+
48
+ **Do not nest a second `<TooltipProvider>` inside library components** —
49
+ that creates a separate context scope and breaks the tooltip↔provider link.
50
+
27
51
  ## Components
28
52
 
29
53
  Organized in `components/` by category — everything re-exported from the root barrel.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.415",
3
+ "version": "2.1.417",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -63,6 +63,11 @@
63
63
  "import": "./src/lib/dialog-service/index.ts",
64
64
  "require": "./src/lib/dialog-service/index.ts"
65
65
  },
66
+ "./providers": {
67
+ "types": "./src/providers/index.ts",
68
+ "import": "./src/providers/index.ts",
69
+ "require": "./src/providers/index.ts"
70
+ },
66
71
  "./utils": {
67
72
  "types": "./src/utils/index.ts",
68
73
  "import": "./src/utils/index.ts",
@@ -95,7 +100,7 @@
95
100
  "check": "tsc --noEmit"
96
101
  },
97
102
  "peerDependencies": {
98
- "@djangocfg/i18n": "^2.1.415",
103
+ "@djangocfg/i18n": "^2.1.417",
99
104
  "consola": "^3.4.2",
100
105
  "lucide-react": "^0.545.0",
101
106
  "moment": "^2.30.1",
@@ -166,8 +171,8 @@
166
171
  "vaul": "1.1.2"
167
172
  },
168
173
  "devDependencies": {
169
- "@djangocfg/i18n": "^2.1.415",
170
- "@djangocfg/typescript-config": "^2.1.415",
174
+ "@djangocfg/i18n": "^2.1.417",
175
+ "@djangocfg/typescript-config": "^2.1.417",
171
176
  "@types/node": "^25.2.3",
172
177
  "@types/react": "^19.2.15",
173
178
  "@types/react-dom": "^19.2.3",
@@ -68,8 +68,7 @@ export { ResponsiveSheet, ResponsiveSheetContent, ResponsiveSheetHeader, Respons
68
68
  export { SidePanel, SidePanelContent, SidePanelHeader, SidePanelTitle, SidePanelDescription, SidePanelBody, SidePanelFooter, SidePanelClose } from './overlay/side-panel';
69
69
  export type { SidePanelProps, SidePanelContentProps, SidePanelCloseProps } from './overlay/side-panel';
70
70
  export { HoverCard, HoverCardContent, HoverCardTrigger } from './overlay/hover-card';
71
- export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, SafeTooltipProvider } from './overlay/tooltip';
72
- export type { SafeTooltipProviderProps } from './overlay/tooltip';
71
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './overlay/tooltip';
73
72
 
74
73
  // ─────────────────────────────────────────────────────────────────────────────
75
74
  // Navigation
@@ -32,5 +32,3 @@ const TooltipContent = React.forwardRef<
32
32
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
33
33
 
34
34
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
35
- export { SafeTooltipProvider } from './tooltip-provider-safe';
36
- export type { SafeTooltipProviderProps } from './tooltip-provider-safe';
package/src/index.ts CHANGED
@@ -14,3 +14,6 @@ export * from './hooks';
14
14
 
15
15
  // Re-export lib utilities
16
16
  export * from './lib';
17
+
18
+ // Re-export composite providers (UiProviders)
19
+ export * from './providers';
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import type { DialogAPI } from './types';
4
+
5
+ /**
6
+ * Lazy accessor for the global `window.dialog` API installed by
7
+ * `<DialogProvider />`. Use this in any library code that needs to call
8
+ * `dialog.confirm` / `dialog.alert` / `dialog.prompt` from a non-React
9
+ * context (e.g. event handlers in `ui-tools` tools).
10
+ *
11
+ * Returns `null` (and warns once in dev) when:
12
+ * - we're rendering on the server (`typeof window === 'undefined'`)
13
+ * - the host app has not mounted `<DialogProvider />`, so `window.dialog`
14
+ * is missing.
15
+ *
16
+ * The caller must handle the `null` case — usually by bailing out of the
17
+ * action with a console warning. Library code must never throw if the
18
+ * provider is missing; consumers without it should still be able to
19
+ * import / render the component (just without CRUD flows).
20
+ *
21
+ * @example
22
+ * const dialog = getDialog();
23
+ * if (!dialog) return;
24
+ * const ok = await dialog.confirm({ title: 'Delete?', message: '…', variant: 'destructive' });
25
+ * if (ok) await onDelete();
26
+ */
27
+ export function getDialog(): DialogAPI | null {
28
+ if (typeof window === 'undefined') return null;
29
+ const api = (window as Window & { dialog?: DialogAPI }).dialog;
30
+ if (!api) {
31
+ if (process.env.NODE_ENV !== 'production' && !warnedMissing) {
32
+ warnedMissing = true;
33
+ // eslint-disable-next-line no-console
34
+ console.warn(
35
+ '[getDialog] window.dialog is not available — mount <DialogProvider /> at the app root.',
36
+ );
37
+ }
38
+ return null;
39
+ }
40
+ return api;
41
+ }
42
+
43
+ let warnedMissing = false;
44
+
45
+ /**
46
+ * Like `getDialog()` but throws when unavailable. Use sparingly — only in
47
+ * code paths where missing `window.dialog` is a developer error (e.g. an
48
+ * action explicitly triggered by user interaction in a feature that
49
+ * unconditionally requires dialogs).
50
+ */
51
+ export function requireDialog(): DialogAPI {
52
+ const api = getDialog();
53
+ if (!api) {
54
+ throw new Error(
55
+ 'requireDialog(): window.dialog is not available. Mount <DialogProvider /> at the app root.',
56
+ );
57
+ }
58
+ return api;
59
+ }
@@ -10,6 +10,9 @@ export type {
10
10
  // Store
11
11
  export { dialogStore, useDialogStore, initDialogAPI, showDialog } from './store';
12
12
 
13
+ // Imperative accessors for non-React code (library helpers, event handlers).
14
+ export { getDialog, requireDialog } from './getDialog';
15
+
13
16
  // Provider
14
17
  export { DialogProvider } from './DialogProvider';
15
18
 
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ import { TooltipProvider } from '../components/overlay/tooltip';
7
+ import { Toaster } from '../components/feedback/sonner';
8
+ import { DialogProvider } from '../lib/dialog-service/DialogProvider';
9
+
10
+ export interface UiProvidersProps {
11
+ children: React.ReactNode;
12
+ /** Tooltip open delay in ms. @default 100 (snappy for desktop apps) */
13
+ tooltipDelay?: number;
14
+ /** Disable the Sonner toaster (e.g. when the host renders its own). */
15
+ noToaster?: boolean;
16
+ /** Disable the imperative `window.dialog` service + its renderer. */
17
+ noDialogService?: boolean;
18
+ /**
19
+ * SSR-safe mount strategy. Default `true` — skips providers on the
20
+ * initial server render and remounts after `useEffect` so Radix
21
+ * `<TooltipProvider>` (which calls `useId()` + opens internal state
22
+ * on mount) doesn't trigger hydration mismatches under Next.js.
23
+ *
24
+ * Set `ssr={false}` on CSR-only hosts (Wails, Vite SPA, Storybook
25
+ * iframe) to skip the deferred mount — providers render on the
26
+ * very first paint and library components see their context
27
+ * immediately.
28
+ */
29
+ ssr?: boolean;
30
+ }
31
+
32
+ /**
33
+ * One root composition for every overlay/imperative-service the
34
+ * djangocfg UI library needs:
35
+ *
36
+ * - `<TooltipProvider>` — single context root for every `<Tooltip>`
37
+ * in the tree (Radix). Without one, library components log
38
+ * "Tooltip must be used within TooltipProvider".
39
+ * - `<DialogProvider>` — installs the `window.dialog.*` imperative
40
+ * API and renders the active dialog into a portal.
41
+ * - `<Toaster>` — Sonner toast portal. Library callsites use
42
+ * `toast(...)` from the same package; no portal = silent no-op.
43
+ *
44
+ * Apple-style: app/host mounts ONE `<UiProviders>` at the very top of
45
+ * the tree, and never again. Library components must NOT include their
46
+ * own nested `TooltipProvider` — a second context root creates a fresh
47
+ * provider scope with different delays, and (worse) under Vite dev a
48
+ * dup-module load yields two `createContext()` instances, breaking the
49
+ * provider/consumer link.
50
+ *
51
+ * Usage:
52
+ * import { UiProviders } from '@djangocfg/ui-core/lib/providers';
53
+ *
54
+ * <UiProviders>
55
+ * <YourApp />
56
+ * </UiProviders>
57
+ */
58
+ export function UiProviders({
59
+ children,
60
+ tooltipDelay = 100,
61
+ noToaster,
62
+ noDialogService,
63
+ }: UiProvidersProps) {
64
+ // No SSR-skip on purpose: any nested library component that renders
65
+ // `<Tooltip>` on its first paint expects to find `<TooltipProvider>`
66
+ // already in the tree. Skipping the provider during SSR caused
67
+ // "Tooltip must be used within TooltipProvider" before hydration.
68
+ // Radix's own provider tolerates the SSR pass — no hydration
69
+ // mismatches observed; the safe wrapper was over-engineering.
70
+ const tree = noDialogService ? children : <DialogProvider>{children}</DialogProvider>;
71
+ return (
72
+ <TooltipProvider delayDuration={tooltipDelay}>
73
+ {tree}
74
+ {!noToaster && <Toaster />}
75
+ </TooltipProvider>
76
+ );
77
+ }
@@ -0,0 +1,2 @@
1
+ export { UiProviders } from './UiProviders';
2
+ export type { UiProvidersProps } from './UiProviders';
@@ -1,44 +0,0 @@
1
- "use client"
2
-
3
- import * as React from 'react';
4
-
5
- import { TooltipProvider as RadixTooltipProvider } from '@radix-ui/react-tooltip';
6
-
7
- export interface SafeTooltipProviderProps {
8
- children: React.ReactNode;
9
- delayDuration?: number;
10
- skipDelayDuration?: number;
11
- disableHoverableContent?: boolean;
12
- }
13
-
14
- /**
15
- * SafeTooltipProvider - SSR-safe wrapper for Radix TooltipProvider
16
- * Only renders on client-side to avoid hydration mismatches
17
- */
18
- export function SafeTooltipProvider({
19
- children,
20
- delayDuration = 700,
21
- skipDelayDuration = 300,
22
- disableHoverableContent,
23
- }: SafeTooltipProviderProps) {
24
- const [mounted, setMounted] = React.useState(false);
25
-
26
- React.useEffect(() => {
27
- setMounted(true);
28
- }, []);
29
-
30
- if (!mounted) {
31
- // During SSR, return children without TooltipProvider
32
- return <>{children}</>;
33
- }
34
-
35
- return (
36
- <RadixTooltipProvider
37
- delayDuration={delayDuration}
38
- skipDelayDuration={skipDelayDuration}
39
- disableHoverableContent={disableHoverableContent}
40
- >
41
- {children}
42
- </RadixTooltipProvider>
43
- );
44
- }