@djangocfg/ui-core 2.1.242 → 2.1.246

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
@@ -149,12 +149,12 @@ hslToRgba('217 91% 60%', 0.5); // 'rgba(59, 130, 246, 0.5)'
149
149
 
150
150
  ## Dialog Service
151
151
 
152
- Universal dialog service to replace native `window.alert`, `window.confirm`, `window.prompt` with beautiful shadcn dialogs.
152
+ Zustand-powered dialog service replacing native `window.alert`, `window.confirm`, `window.prompt` with shadcn dialogs. Also provides `window.dialog.auth()` for triggering authentication dialogs.
153
153
 
154
154
  ```tsx
155
- import { DialogProvider, useDialog, dialog } from '@djangocfg/ui-core/lib/dialog-service';
155
+ import { DialogProvider, useDialog } from '@djangocfg/ui-core/lib/dialog-service';
156
156
 
157
- // Wrap your app with DialogProvider
157
+ // Wrap your app with DialogProvider (already included in BaseApp)
158
158
  function App() {
159
159
  return (
160
160
  <DialogProvider>
@@ -165,7 +165,7 @@ function App() {
165
165
 
166
166
  // Use via React hook
167
167
  function Component() {
168
- const { alert, confirm, prompt } = useDialog();
168
+ const { alert, confirm, prompt, auth } = useDialog();
169
169
 
170
170
  const handleDelete = async () => {
171
171
  const confirmed = await confirm({
@@ -177,12 +177,20 @@ function Component() {
177
177
  // Delete...
178
178
  }
179
179
  };
180
+
181
+ const handleProtected = async () => {
182
+ const didAuth = await auth({ message: 'Please sign in to continue' });
183
+ if (didAuth) {
184
+ // User navigated to auth
185
+ }
186
+ };
180
187
  }
181
188
 
182
189
  // Or use globally from anywhere (vanilla JS, libraries, etc.)
183
- dialog.alert({ message: 'Hello!' });
184
- const ok = await dialog.confirm({ message: 'Are you sure?' });
185
- const name = await dialog.prompt({ message: 'Enter your name:' });
190
+ window.dialog.alert({ message: 'Hello!' });
191
+ const ok = await window.dialog.confirm({ message: 'Are you sure?' });
192
+ const name = await window.dialog.prompt({ message: 'Enter your name:' });
193
+ const didAuth = await window.dialog.auth({ message: 'Session expired' });
186
194
  ```
187
195
 
188
196
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.242",
3
+ "version": "2.1.246",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -81,7 +81,7 @@
81
81
  "playground": "playground dev"
82
82
  },
83
83
  "peerDependencies": {
84
- "@djangocfg/i18n": "^2.1.242",
84
+ "@djangocfg/i18n": "^2.1.246",
85
85
  "consola": "^3.4.2",
86
86
  "lucide-react": "^0.545.0",
87
87
  "moment": "^2.30.1",
@@ -143,9 +143,9 @@
143
143
  "vaul": "1.1.2"
144
144
  },
145
145
  "devDependencies": {
146
- "@djangocfg/i18n": "^2.1.242",
146
+ "@djangocfg/i18n": "^2.1.246",
147
147
  "@djangocfg/playground": "workspace:*",
148
- "@djangocfg/typescript-config": "^2.1.242",
148
+ "@djangocfg/typescript-config": "^2.1.246",
149
149
  "@types/node": "^24.7.2",
150
150
  "@types/react": "^19.1.0",
151
151
  "@types/react-dom": "^19.1.0",
@@ -1,73 +1,34 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState, useCallback, type ReactNode } from 'react';
3
+ import { useEffect, useCallback, type ReactNode } from 'react';
4
4
  import { AlertDialogUI, ConfirmDialogUI, PromptDialogUI } from './dialogs';
5
- import { initDialogAPI, dispatchDialogResponse } from './events';
6
- import { DIALOG_REQUEST_EVENT } from './constants';
7
- import type { DialogRequest, DialogRequestPayload } from './types';
5
+ import { initDialogAPI, useDialogStore } from './store';
8
6
 
9
7
  interface DialogProviderProps {
10
8
  children: ReactNode;
11
9
  }
12
10
 
13
11
  /**
14
- * DialogProvider - Listens for dialog CustomEvents and renders appropriate dialogs
12
+ * DialogProvider - Reads from zustand dialog store and renders appropriate dialogs
15
13
  *
16
14
  * Must be mounted once at the app root (e.g., in BaseApp).
17
15
  * Handles dialog queue to show one dialog at a time.
18
16
  */
19
17
  export function DialogProvider({ children }: DialogProviderProps) {
20
- const [queue, setQueue] = useState<DialogRequest[]>([]);
21
- const [current, setCurrent] = useState<DialogRequest | null>(null);
18
+ const current = useDialogStore((s) => s.current);
19
+ const resolve = useDialogStore((s) => s.resolve);
22
20
 
23
- // Initialize global API on mount
21
+ // Initialize global window.dialog API on mount
24
22
  useEffect(() => {
25
23
  initDialogAPI();
26
24
  }, []);
27
25
 
28
- // Listen for dialog requests
29
- useEffect(() => {
30
- const handleRequest = (event: Event) => {
31
- const customEvent = event as CustomEvent<DialogRequestPayload>;
32
- const { id, type, options } = customEvent.detail;
33
-
34
- const request: DialogRequest = {
35
- id,
36
- type,
37
- options,
38
- resolve: (result) => {
39
- dispatchDialogResponse(id, result);
40
- },
41
- };
42
-
43
- setQueue((prev) => [...prev, request]);
44
- };
45
-
46
- window.addEventListener(DIALOG_REQUEST_EVENT, handleRequest);
47
-
48
- return () => {
49
- window.removeEventListener(DIALOG_REQUEST_EVENT, handleRequest);
50
- };
51
- }, []);
52
-
53
- // Process queue - show one dialog at a time
54
- useEffect(() => {
55
- if (!current && queue.length > 0) {
56
- const [next, ...rest] = queue;
57
- setCurrent(next ?? null);
58
- setQueue(rest);
59
- }
60
- }, [current, queue]);
61
-
62
26
  // Handle dialog close
63
27
  const handleClose = useCallback(
64
28
  (result: boolean | string | null) => {
65
- if (current) {
66
- current.resolve(result);
67
- setCurrent(null);
68
- }
29
+ resolve(result);
69
30
  },
70
- [current]
31
+ [resolve],
71
32
  );
72
33
 
73
34
  // Render current dialog
@@ -1,17 +1,18 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback } from 'react';
4
- import type { DialogOptions } from '../types';
4
+ import { showDialog, dialogStore } from '../store';
5
+ import type { DialogOptions, AuthDialogOptions } from '../types';
5
6
 
6
7
  /**
7
8
  * React hook for using dialog service
8
9
  *
9
- * Provides type-safe access to window.dialog with fallback to native dialogs.
10
+ * Provides type-safe access to dialogs via zustand store with fallback to native dialogs.
10
11
  *
11
12
  * @example
12
13
  * ```tsx
13
14
  * function MyComponent() {
14
- * const { confirm, alert, prompt } = useDialog();
15
+ * const { confirm, alert, prompt, auth } = useDialog();
15
16
  *
16
17
  * const handleDelete = async () => {
17
18
  * const confirmed = await confirm({
@@ -24,6 +25,13 @@ import type { DialogOptions } from '../types';
24
25
  * }
25
26
  * };
26
27
  *
28
+ * const handleProtected = async () => {
29
+ * const authed = await auth({ message: 'Please sign in to continue' });
30
+ * if (authed) {
31
+ * // proceed
32
+ * }
33
+ * };
34
+ *
27
35
  * return <button onClick={handleDelete}>Delete</button>;
28
36
  * }
29
37
  * ```
@@ -35,13 +43,12 @@ export function useDialog() {
35
43
  return Promise.resolve();
36
44
  }
37
45
  if (!window.dialog) {
38
- // Fallback to native
39
46
  window.alert(typeof message === 'string' ? message : message.message);
40
47
  return Promise.resolve();
41
48
  }
42
- return window.dialog.alert(message);
49
+ return showDialog('alert', message).then(() => undefined);
43
50
  },
44
- []
51
+ [],
45
52
  );
46
53
 
47
54
  const confirm = useCallback(
@@ -50,14 +57,13 @@ export function useDialog() {
50
57
  return Promise.resolve(false);
51
58
  }
52
59
  if (!window.dialog) {
53
- // Fallback to native
54
60
  return Promise.resolve(
55
- window.confirm(typeof message === 'string' ? message : message.message)
61
+ window.confirm(typeof message === 'string' ? message : message.message),
56
62
  );
57
63
  }
58
- return window.dialog.confirm(message);
64
+ return showDialog('confirm', message) as Promise<boolean>;
59
65
  },
60
- []
66
+ [],
61
67
  );
62
68
 
63
69
  const prompt = useCallback(
@@ -66,14 +72,20 @@ export function useDialog() {
66
72
  return Promise.resolve(null);
67
73
  }
68
74
  if (!window.dialog) {
69
- // Fallback to native
70
75
  const opts = typeof message === 'string' ? { message } : message;
71
76
  return Promise.resolve(window.prompt(opts.message, opts.defaultValue));
72
77
  }
73
- return window.dialog.prompt(message);
78
+ return showDialog('prompt', message) as Promise<string | null>;
79
+ },
80
+ [],
81
+ );
82
+
83
+ const auth = useCallback(
84
+ (options?: AuthDialogOptions): Promise<boolean> => {
85
+ return dialogStore.getState().openAuth(options);
74
86
  },
75
- []
87
+ [],
76
88
  );
77
89
 
78
- return { alert, confirm, prompt };
90
+ return { alert, confirm, prompt, auth };
79
91
  }
@@ -3,14 +3,15 @@ export type {
3
3
  DialogType,
4
4
  DialogVariant,
5
5
  DialogOptions,
6
+ AuthDialogOptions,
6
7
  DialogAPI,
7
8
  } from './types';
8
9
 
10
+ // Store
11
+ export { dialogStore, useDialogStore, initDialogAPI, showDialog } from './store';
12
+
9
13
  // Provider
10
14
  export { DialogProvider } from './DialogProvider';
11
15
 
12
16
  // Hooks
13
17
  export { useDialog } from './hooks';
14
-
15
- // Events (for advanced usage)
16
- export { initDialogAPI } from './events';
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { createStore, useStore } from 'zustand';
4
+ import type { DialogRequest, DialogType, DialogOptions, AuthDialogOptions } from './types';
5
+
6
+ // ---------- store shape ----------
7
+
8
+ interface DialogState {
9
+ /** Queue of pending dialogs */
10
+ queue: DialogRequest[];
11
+ /** Currently displayed dialog */
12
+ current: DialogRequest | null;
13
+
14
+ /** Auth dialog state (separate — rendered by layouts, not dialog-service) */
15
+ authOpen: boolean;
16
+ authOptions: AuthDialogOptions | null;
17
+ authResolve: ((value: boolean) => void) | null;
18
+ }
19
+
20
+ interface DialogActions {
21
+ /** Push a standard dialog (alert/confirm/prompt) into the queue */
22
+ enqueue: (request: DialogRequest) => void;
23
+ /** Resolve & close the current standard dialog */
24
+ resolve: (result: boolean | string | null) => void;
25
+
26
+ /** Open auth dialog, returns promise resolved on close */
27
+ openAuth: (options?: AuthDialogOptions) => Promise<boolean>;
28
+ /** Resolve auth dialog (called by AuthDialog component) */
29
+ resolveAuth: (result: boolean) => void;
30
+ }
31
+
32
+ type DialogStore = DialogState & DialogActions;
33
+
34
+ // ---------- singleton store ----------
35
+
36
+ let idCounter = 0;
37
+ function generateId(): string {
38
+ return `dialog-${Date.now()}-${++idCounter}`;
39
+ }
40
+
41
+ export const dialogStore = createStore<DialogStore>((set, get) => ({
42
+ // state
43
+ queue: [],
44
+ current: null,
45
+ authOpen: false,
46
+ authOptions: null,
47
+ authResolve: null,
48
+
49
+ // actions
50
+ enqueue: (request) => {
51
+ set((s) => {
52
+ if (!s.current) {
53
+ return { current: request };
54
+ }
55
+ return { queue: [...s.queue, request] };
56
+ });
57
+ },
58
+
59
+ resolve: (result) => {
60
+ const { current, queue } = get();
61
+ if (!current) return;
62
+ current.resolve(result);
63
+ const [next, ...rest] = queue;
64
+ set({ current: next ?? null, queue: rest });
65
+ },
66
+
67
+ openAuth: (options) => {
68
+ return new Promise<boolean>((resolve) => {
69
+ set({ authOpen: true, authOptions: options ?? null, authResolve: resolve });
70
+ });
71
+ },
72
+
73
+ resolveAuth: (result) => {
74
+ const { authResolve } = get();
75
+ authResolve?.(result);
76
+ set({ authOpen: false, authOptions: null, authResolve: null });
77
+ },
78
+ }));
79
+
80
+ // ---------- helpers ----------
81
+
82
+ /**
83
+ * Show a standard dialog (alert/confirm/prompt) via the store.
84
+ * Returns a promise that resolves when the user responds.
85
+ */
86
+ export function showDialog(
87
+ type: DialogType,
88
+ messageOrOptions: string | DialogOptions,
89
+ ): Promise<boolean | string | null> {
90
+ if (type === 'auth') {
91
+ return dialogStore.getState().openAuth(
92
+ typeof messageOrOptions === 'string'
93
+ ? { message: messageOrOptions }
94
+ : { message: messageOrOptions.message },
95
+ );
96
+ }
97
+
98
+ return new Promise((resolve) => {
99
+ const options: DialogOptions =
100
+ typeof messageOrOptions === 'string'
101
+ ? { message: messageOrOptions }
102
+ : messageOrOptions;
103
+
104
+ const request: DialogRequest = {
105
+ id: generateId(),
106
+ type,
107
+ options,
108
+ resolve,
109
+ };
110
+
111
+ dialogStore.getState().enqueue(request);
112
+ });
113
+ }
114
+
115
+ // ---------- window.dialog init ----------
116
+
117
+ export function initDialogAPI(): void {
118
+ if (typeof window === 'undefined') return;
119
+ if (window.dialog) return;
120
+
121
+ window.dialog = {
122
+ alert: (message) => showDialog('alert', message).then(() => undefined),
123
+ confirm: (message) => showDialog('confirm', message) as Promise<boolean>,
124
+ prompt: (message) => showDialog('prompt', message) as Promise<string | null>,
125
+ auth: (options) => dialogStore.getState().openAuth(options),
126
+ };
127
+ }
128
+
129
+ // ---------- react hooks ----------
130
+
131
+ export function useDialogStore<T>(selector: (state: DialogStore) => T): T {
132
+ return useStore(dialogStore, selector);
133
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
- export type DialogType = 'alert' | 'confirm' | 'prompt';
3
+ export type DialogType = 'alert' | 'confirm' | 'prompt' | 'auth';
4
4
 
5
5
  export type DialogVariant = 'default' | 'destructive' | 'warning' | 'success';
6
6
 
@@ -27,22 +27,18 @@ export interface DialogOptions {
27
27
  preventClose?: boolean;
28
28
  }
29
29
 
30
- export interface DialogRequest {
31
- id: string;
32
- type: DialogType;
33
- options: DialogOptions;
34
- resolve: (value: boolean | string | null) => void;
30
+ export interface AuthDialogOptions {
31
+ /** Custom message to display */
32
+ message?: string;
33
+ /** URL to redirect after auth */
34
+ redirectUrl?: string;
35
35
  }
36
36
 
37
- export interface DialogRequestPayload {
37
+ export interface DialogRequest {
38
38
  id: string;
39
39
  type: DialogType;
40
40
  options: DialogOptions;
41
- }
42
-
43
- export interface DialogResponsePayload {
44
- id: string;
45
- result: boolean | string | null;
41
+ resolve: (value: boolean | string | null) => void;
46
42
  }
47
43
 
48
44
  /** Global window.dialog API */
@@ -50,6 +46,7 @@ export interface DialogAPI {
50
46
  alert: (message: string | DialogOptions) => Promise<void>;
51
47
  confirm: (message: string | DialogOptions) => Promise<boolean>;
52
48
  prompt: (message: string | DialogOptions) => Promise<string | null>;
49
+ auth: (options?: AuthDialogOptions) => Promise<boolean>;
53
50
  }
54
51
 
55
52
  declare global {
@@ -1,73 +0,0 @@
1
- import { DIALOG_REQUEST_EVENT, DIALOG_RESPONSE_EVENT } from './constants';
2
- import type {
3
- DialogType,
4
- DialogOptions,
5
- DialogRequestPayload,
6
- DialogResponsePayload,
7
- } from './types';
8
-
9
- let dialogIdCounter = 0;
10
-
11
- function generateId(): string {
12
- return `dialog-${Date.now()}-${++dialogIdCounter}`;
13
- }
14
-
15
- /**
16
- * Dispatch dialog request and wait for response via CustomEvent
17
- */
18
- function showDialog(
19
- type: DialogType,
20
- messageOrOptions: string | DialogOptions
21
- ): Promise<boolean | string | null> {
22
- return new Promise((resolve) => {
23
- const id = generateId();
24
- const options: DialogOptions =
25
- typeof messageOrOptions === 'string'
26
- ? { message: messageOrOptions }
27
- : messageOrOptions;
28
-
29
- // Listen for response
30
- const handleResponse = (event: Event) => {
31
- const customEvent = event as CustomEvent<DialogResponsePayload>;
32
- if (customEvent.detail.id === id) {
33
- window.removeEventListener(DIALOG_RESPONSE_EVENT, handleResponse);
34
- resolve(customEvent.detail.result);
35
- }
36
- };
37
-
38
- window.addEventListener(DIALOG_RESPONSE_EVENT, handleResponse);
39
-
40
- // Dispatch request
41
- window.dispatchEvent(
42
- new CustomEvent<DialogRequestPayload>(DIALOG_REQUEST_EVENT, {
43
- detail: { id, type, options },
44
- })
45
- );
46
- });
47
- }
48
-
49
- /**
50
- * Initialize global window.dialog API
51
- * Uses singleton pattern - only initializes once
52
- */
53
- export function initDialogAPI(): void {
54
- if (typeof window === 'undefined') return;
55
- if (window.dialog) return; // Already initialized
56
-
57
- window.dialog = {
58
- alert: (message) => showDialog('alert', message).then(() => undefined),
59
- confirm: (message) => showDialog('confirm', message) as Promise<boolean>,
60
- prompt: (message) => showDialog('prompt', message) as Promise<string | null>,
61
- };
62
- }
63
-
64
- /**
65
- * Dispatch response event (used by DialogProvider)
66
- */
67
- export function dispatchDialogResponse(id: string, result: boolean | string | null): void {
68
- window.dispatchEvent(
69
- new CustomEvent<DialogResponsePayload>(DIALOG_RESPONSE_EVENT, {
70
- detail: { id, result },
71
- })
72
- );
73
- }