@donotdev/ui 0.0.6 → 0.0.8
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/assets/fonts/fonts.css +4 -4
- package/dist/components/auth/AuthHeader.d.ts +5 -0
- package/dist/components/auth/AuthHeader.d.ts.map +1 -1
- package/dist/components/auth/AuthHeader.js +22 -8
- package/dist/components/auth/AuthMenu.d.ts +6 -2
- package/dist/components/auth/AuthMenu.d.ts.map +1 -1
- package/dist/components/auth/AuthMenu.js +2 -2
- package/dist/components/common/RedirectOverlay.d.ts +37 -0
- package/dist/components/common/RedirectOverlay.d.ts.map +1 -0
- package/dist/components/common/RedirectOverlay.js +243 -0
- package/dist/components/common/index.d.ts +1 -0
- package/dist/components/common/index.d.ts.map +1 -1
- package/dist/components/common/index.js +1 -0
- package/dist/components/layout/components/header/SettingsMenu.d.ts.map +1 -1
- package/dist/components/layout/components/header/SettingsMenu.js +1 -1
- package/dist/crud/components/DisplayFieldRenderer.js +1 -1
- package/dist/dndev.css +517 -90
- package/dist/index.js +4 -4
- package/dist/internal/devtools/components/DesignTab.d.ts.map +1 -1
- package/dist/internal/devtools/components/DesignTab.js +25 -3
- package/dist/internal/layout/components/PerformanceHints.d.ts +64 -0
- package/dist/internal/layout/components/PerformanceHints.d.ts.map +1 -0
- package/dist/internal/layout/components/PerformanceHints.js +88 -0
- package/dist/internal/layout/components/footer/FooterBranding.js +1 -1
- package/dist/internal/layout/config/presets/admin.d.ts +2 -2
- package/dist/internal/layout/config/presets/admin.d.ts.map +1 -1
- package/dist/internal/layout/config/presets/admin.js +8 -4
- package/dist/providers/NextJsAppProviders.d.ts.map +1 -1
- package/dist/providers/NextJsAppProviders.js +5 -1
- package/dist/providers/ViteAppProviders.d.ts.map +1 -1
- package/dist/providers/ViteAppProviders.js +4 -1
- package/dist/routing/hooks/hooks.next.d.ts +1 -0
- package/dist/routing/hooks/hooks.next.d.ts.map +1 -1
- package/dist/routing/hooks/hooks.next.js +1 -1
- package/dist/routing/hooks/hooks.vite.d.ts +1 -0
- package/dist/routing/hooks/hooks.vite.d.ts.map +1 -1
- package/dist/routing/hooks/hooks.vite.js +1 -1
- package/dist/routing/hooks/useRouteParam.next.d.ts +18 -0
- package/dist/routing/hooks/useRouteParam.next.d.ts.map +1 -0
- package/dist/routing/hooks/useRouteParam.next.js +38 -0
- package/dist/routing/hooks/useRouteParam.vite.d.ts +18 -0
- package/dist/routing/hooks/useRouteParam.vite.d.ts.map +1 -0
- package/dist/routing/hooks/useRouteParam.vite.js +38 -0
- package/dist/routing/index.d.ts +1 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/useGoTo.d.ts +3 -4
- package/dist/routing/useGoTo.d.ts.map +1 -1
- package/dist/routing/useGoTo.js +16 -23
- package/dist/styles/index.css +513 -86
- package/dist/utils/useCrudSafe.d.ts.map +1 -1
- package/dist/utils/useCrudSafe.js +2 -1
- package/dist/vite-routing/RootLayout.d.ts.map +1 -1
- package/dist/vite-routing/RootLayout.js +4 -1
- package/package.json +10 -11
package/assets/fonts/fonts.css
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
font-family: Roboto;
|
|
5
5
|
font-style: normal;
|
|
6
6
|
font-weight: 400;
|
|
7
|
-
font-display:
|
|
7
|
+
font-display: swap;
|
|
8
8
|
src: url('/fonts/Roboto-400-latin.woff2') format('woff2');
|
|
9
9
|
unicode-range: U+0000-00FF; /* Basic Latin - loads first for performance */
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
font-family: Roboto;
|
|
14
14
|
font-style: normal;
|
|
15
15
|
font-weight: 700;
|
|
16
|
-
font-display:
|
|
16
|
+
font-display: swap;
|
|
17
17
|
src: url('/fonts/Roboto-700-latin.woff2') format('woff2');
|
|
18
18
|
unicode-range: U+0000-00FF; /* Basic Latin - loads first for performance */
|
|
19
19
|
}
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
font-family: Inter;
|
|
154
154
|
font-style: normal;
|
|
155
155
|
font-weight: 400 700;
|
|
156
|
-
font-display: optional
|
|
156
|
+
font-display: swap; /* Changed from optional to swap - we preload this font */
|
|
157
157
|
src: url('/fonts/Inter-latin.woff2') format('woff2');
|
|
158
158
|
unicode-range:
|
|
159
159
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
font-family: 'Playfair Display';
|
|
185
185
|
font-style: normal;
|
|
186
186
|
font-weight: 400 700;
|
|
187
|
-
font-display:
|
|
187
|
+
font-display: swap;
|
|
188
188
|
src: url('/fonts/PlayfairDisplay-latin.woff2') format('woff2');
|
|
189
189
|
unicode-range:
|
|
190
190
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
1
2
|
import { DISPLAY } from '@donotdev/components';
|
|
2
3
|
import type { ComponentType } from 'react';
|
|
3
4
|
/**
|
|
@@ -46,6 +47,10 @@ export interface AuthHeaderProps {
|
|
|
46
47
|
* @default 'auto'
|
|
47
48
|
*/
|
|
48
49
|
display?: (typeof DISPLAY)[keyof typeof DISPLAY];
|
|
50
|
+
/** Optional className for styling the button */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Optional inline styles for the button */
|
|
53
|
+
style?: CSSProperties;
|
|
49
54
|
}
|
|
50
55
|
/**
|
|
51
56
|
* Auth header component
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthHeader.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthHeader.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AuthHeader.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthHeader.tsx"],"names":[],"mappings":"AAcA,OAAc,EAA4B,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE5E,OAAO,EAKL,OAAO,EACR,MAAM,sBAAsB,CAAC;AAU9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAyB3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;IACjD,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,EAAE,aAAa,CAAC,eAAe,CA4IrD,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
'use client';
|
|
11
11
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
12
|
import { User } from 'lucide-react';
|
|
13
|
-
import React, { Suspense, lazy } from 'react';
|
|
14
|
-
import { Button, BUTTON_VARIANT, DropdownMenu, DISPLAY, } from '@donotdev/components';
|
|
13
|
+
import React, { Suspense, lazy, useState } from 'react';
|
|
14
|
+
import { Button, BUTTON_VARIANT, DropdownMenu, Dialog, DISPLAY, } from '@donotdev/components';
|
|
15
15
|
import { useTranslation } from '@donotdev/core';
|
|
16
16
|
import { useAuthConfig } from '@donotdev/core';
|
|
17
17
|
import { getEnabledAuthPartners } from '@donotdev/core';
|
|
@@ -47,34 +47,48 @@ const MultipleAuthProviders = lazy(async () => {
|
|
|
47
47
|
* @since 0.0.1
|
|
48
48
|
* @author AMBROISE PARK Consulting
|
|
49
49
|
*/
|
|
50
|
-
export const AuthHeader = ({ display = DISPLAY.AUTO, }) => {
|
|
50
|
+
export const AuthHeader = ({ display = DISPLAY.AUTO, className, style, }) => {
|
|
51
51
|
const { t } = useTranslation('dndev');
|
|
52
52
|
const { shouldHide, isLoading, isReady, isAuthenticated } = useAuthVisibility();
|
|
53
53
|
const authConfig = useAuthConfig();
|
|
54
54
|
const enabled = getEnabledAuthPartners();
|
|
55
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
55
56
|
const loginPath = authConfig.loginPath;
|
|
56
57
|
const signInLabel = t('auth.signIn', { defaultValue: 'Sign In' });
|
|
57
58
|
const loadingLabel = t('auth.loading', { defaultValue: 'Loading...' });
|
|
59
|
+
const hasPasswordProvider = enabled.includes('password') || enabled.includes('emailLink');
|
|
58
60
|
if (shouldHide || !isReady) {
|
|
59
61
|
return null;
|
|
60
62
|
}
|
|
63
|
+
// Local component for Sign In button (DRY)
|
|
64
|
+
const SignInButton = ({ children, ...props }) => (_jsx(Button, { variant: BUTTON_VARIANT.OUTLINE, icon: User, display: display, tooltip: signInLabel, className: className, style: style, ...props, children: children ?? signInLabel }));
|
|
61
65
|
// NOT AUTHENTICATED
|
|
62
66
|
// Button's display prop + CSS container queries handle responsive label visibility
|
|
63
67
|
if (!isAuthenticated) {
|
|
64
68
|
if (loginPath !== undefined) {
|
|
65
|
-
return (_jsx(Link, { path: loginPath, prefetch: true, children: _jsx(
|
|
69
|
+
return (_jsx(Link, { path: loginPath, prefetch: true, children: _jsx(SignInButton, {}) }));
|
|
66
70
|
}
|
|
71
|
+
const targetProvider = enabled[0];
|
|
72
|
+
// Use Dialog if password/emailLink provider is present (better UX for forms)
|
|
73
|
+
if (hasPasswordProvider) {
|
|
74
|
+
return (_jsx(Dialog, { trigger: _jsx(SignInButton, {}), title: signInLabel, open: dialogOpen, onOpenChange: setDialogOpen, "data-content-size": "form", children: _jsx(Suspense, { fallback: _jsx("div", { style: {
|
|
75
|
+
padding: 'var(--gap-md)',
|
|
76
|
+
fontSize: 'var(--font-size-sm)',
|
|
77
|
+
color: 'var(--muted-foreground)',
|
|
78
|
+
}, children: loadingLabel }), children: _jsx(MultipleAuthProviders, { spacing: "medium", onSuccess: () => setDialogOpen(false) }) }) }));
|
|
79
|
+
}
|
|
80
|
+
// Use DropdownMenu if multiple OAuth providers (no password/emailLink)
|
|
67
81
|
if (enabled.length > 1) {
|
|
68
|
-
return (_jsx(DropdownMenu, { trigger: _jsx(
|
|
82
|
+
return (_jsx(DropdownMenu, { trigger: _jsx(SignInButton, {}), children: _jsx(Suspense, { fallback: _jsx("div", { style: {
|
|
69
83
|
padding: 'var(--gap-md)',
|
|
70
84
|
fontSize: 'var(--font-size-sm)',
|
|
71
85
|
color: 'var(--muted-foreground)',
|
|
72
86
|
}, children: loadingLabel }), children: _jsx(MultipleAuthProviders, { spacing: "tight" }) }) }));
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
return (_jsx(Suspense, { fallback: _jsx(
|
|
88
|
+
// Normal case: single non-password provider → use AuthPartnerButton directly
|
|
89
|
+
return (_jsx(Suspense, { fallback: _jsx(SignInButton, { disabled: isLoading, tooltip: isLoading ? loadingLabel : signInLabel, children: isLoading ? loadingLabel : signInLabel }), children: _jsx(AuthPartnerButton, { partnerId: targetProvider, display: display, className: className, style: style }) }));
|
|
76
90
|
}
|
|
77
91
|
// AUTHENTICATED - Delegate to AuthMenu
|
|
78
|
-
return _jsx(AuthMenu, { display: display, loginPath: loginPath });
|
|
92
|
+
return (_jsx(AuthMenu, { display: display, loginPath: loginPath, className: className, style: style }));
|
|
79
93
|
};
|
|
80
94
|
export default AuthHeader;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { type CSSProperties } from 'react';
|
|
2
2
|
import { DISPLAY } from '@donotdev/components';
|
|
3
3
|
import type { ComponentType } from 'react';
|
|
4
4
|
/**
|
|
@@ -53,7 +53,11 @@ export interface AuthMenuProps {
|
|
|
53
53
|
* Order: Profile (if configured) → authMenuItems → customItems → Delete Account → Sign Out
|
|
54
54
|
*/
|
|
55
55
|
customItems?: AuthMenuCustomItem[];
|
|
56
|
+
/** Optional className for styling the button */
|
|
57
|
+
className?: string;
|
|
58
|
+
/** Optional inline styles for the button */
|
|
59
|
+
style?: CSSProperties;
|
|
56
60
|
}
|
|
57
|
-
export declare const AuthMenu: ({ loginPath, display, "no-tooltip": noTooltip, customItems, }: AuthMenuProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
61
|
+
export declare const AuthMenu: ({ loginPath, display, "no-tooltip": noTooltip, customItems, className, style, }: AuthMenuProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
58
62
|
export default AuthMenu;
|
|
59
63
|
//# sourceMappingURL=AuthMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthMenu.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthMenu.tsx"],"names":[],"mappings":"AA4EA,OAAO,
|
|
1
|
+
{"version":3,"file":"AuthMenu.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthMenu.tsx"],"names":[],"mappings":"AA4EA,OAAO,KAAK,EAAE,EAA2B,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3E,OAAO,EAML,OAAO,EACR,MAAM,sBAAsB,CAAC;AAc9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAqE3C;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mBAAmB;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;IAEjD,sBAAsB;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAEnC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAsBD,eAAO,MAAM,QAAQ,GAAI,iFAOtB,aAAa,mDA0Of,CAAC;AAEF,eAAe,QAAQ,CAAC"}
|
|
@@ -154,7 +154,7 @@ const getIcon = (iconName) => {
|
|
|
154
154
|
const cleanIconName = iconName.replace(/['"]/g, '');
|
|
155
155
|
return LucideIcons[cleanIconName] || Settings;
|
|
156
156
|
};
|
|
157
|
-
export const AuthMenu = ({ loginPath, display = DISPLAY.AUTO, 'no-tooltip': noTooltip = false, customItems = [], }) => {
|
|
157
|
+
export const AuthMenu = ({ loginPath, display = DISPLAY.AUTO, 'no-tooltip': noTooltip = false, customItems = [], className, style, }) => {
|
|
158
158
|
const { t } = useTranslation('dndev');
|
|
159
159
|
const { shouldHide, isReady, isAuthenticated } = useAuthVisibility();
|
|
160
160
|
const authConfig = useAuthConfig();
|
|
@@ -312,6 +312,6 @@ export const AuthMenu = ({ loginPath, display = DISPLAY.AUTO, 'no-tooltip': noTo
|
|
|
312
312
|
? getUserTooltip()
|
|
313
313
|
: !noTooltip
|
|
314
314
|
? t('auth.userMenu', { defaultValue: 'User menu' })
|
|
315
|
-
: undefined, children: getUserDisplayName() }), items: menuItems, contentAlign: "end" })] }));
|
|
315
|
+
: undefined, className: className, style: style, children: getUserDisplayName() }), items: menuItems, contentAlign: "end" })] }));
|
|
316
316
|
};
|
|
317
317
|
export default AuthMenu;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedirectOverlay - Industry-standard redirect overlay with phase progression
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Phase-based progress: connecting → preparing → redirecting → timeout
|
|
6
|
+
* - Cancel button appears after 10s (configurable)
|
|
7
|
+
* - i18n support with operation-specific messaging
|
|
8
|
+
* - Browser event handling (popstate, visibilitychange)
|
|
9
|
+
* - Accessible (ARIA labels, role="status")
|
|
10
|
+
* - RTL-safe styling
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Include once in app root (layout.tsx or App.tsx)
|
|
15
|
+
* import { RedirectOverlay } from '@donotdev/ui';
|
|
16
|
+
*
|
|
17
|
+
* function RootLayout({ children }) {
|
|
18
|
+
* return (
|
|
19
|
+
* <>
|
|
20
|
+
* {children}
|
|
21
|
+
* <RedirectOverlay />
|
|
22
|
+
* </>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* // Hooks trigger it automatically
|
|
27
|
+
* const checkout = useStripeBilling('checkout', authState);
|
|
28
|
+
* await checkout({ priceId: 'price_123' }); // Shows overlay automatically
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @version 0.0.1
|
|
32
|
+
* @since 0.0.1
|
|
33
|
+
* @author AMBROISE PARK Consulting
|
|
34
|
+
*/
|
|
35
|
+
export declare function RedirectOverlay(): import("react/jsx-runtime").JSX.Element | null;
|
|
36
|
+
export default RedirectOverlay;
|
|
37
|
+
//# sourceMappingURL=RedirectOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedirectOverlay.d.ts","sourceRoot":"","sources":["../../../src/components/common/RedirectOverlay.tsx"],"names":[],"mappings":"AAoEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,eAAe,mDA+P9B;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// packages/ui/src/components/common/RedirectOverlay.tsx
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview RedirectOverlay component
|
|
6
|
+
* @description Industry-standard fullscreen overlay for redirect operations (payments, OAuth, etc.).
|
|
7
|
+
* Features phase-based progress, 10s cancel timeout, i18n support, and browser event handling.
|
|
8
|
+
*
|
|
9
|
+
* @version 0.0.1
|
|
10
|
+
* @since 0.0.1
|
|
11
|
+
* @author AMBROISE PARK Consulting
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
14
|
+
import { Lock, Shield } from 'lucide-react';
|
|
15
|
+
import { Portal, Spinner, Stack, Text, Button } from '@donotdev/components';
|
|
16
|
+
import { useTranslation, useOverlay, } from '@donotdev/core';
|
|
17
|
+
/** Phase progression timings (ms from start) */
|
|
18
|
+
const PHASE_TIMINGS = {
|
|
19
|
+
connecting: 0,
|
|
20
|
+
preparing: 2000,
|
|
21
|
+
redirecting: 5000,
|
|
22
|
+
};
|
|
23
|
+
/** Default timeout before showing cancel button (10 seconds) */
|
|
24
|
+
const DEFAULT_CANCEL_TIMEOUT = 10000;
|
|
25
|
+
/**
|
|
26
|
+
* Get icon component based on operation type
|
|
27
|
+
*/
|
|
28
|
+
function getOperationIcon(operation, configIcon) {
|
|
29
|
+
if (configIcon === 'none')
|
|
30
|
+
return null;
|
|
31
|
+
if (configIcon === 'shield')
|
|
32
|
+
return Shield;
|
|
33
|
+
if (configIcon === 'lock')
|
|
34
|
+
return Lock;
|
|
35
|
+
// Infer from operation
|
|
36
|
+
if (!operation)
|
|
37
|
+
return Lock;
|
|
38
|
+
if (operation.startsWith('stripe-') ||
|
|
39
|
+
operation.includes('payment') ||
|
|
40
|
+
operation.includes('checkout')) {
|
|
41
|
+
return Lock;
|
|
42
|
+
}
|
|
43
|
+
if (operation.startsWith('oauth-') || operation.startsWith('auth-')) {
|
|
44
|
+
return Shield;
|
|
45
|
+
}
|
|
46
|
+
return Lock;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get i18n key for operation
|
|
50
|
+
*/
|
|
51
|
+
function getOperationKey(operation) {
|
|
52
|
+
if (!operation)
|
|
53
|
+
return 'default';
|
|
54
|
+
// Check if operation has a specific translation, otherwise use default
|
|
55
|
+
return operation;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* RedirectOverlay - Industry-standard redirect overlay with phase progression
|
|
59
|
+
*
|
|
60
|
+
* Features:
|
|
61
|
+
* - Phase-based progress: connecting → preparing → redirecting → timeout
|
|
62
|
+
* - Cancel button appears after 10s (configurable)
|
|
63
|
+
* - i18n support with operation-specific messaging
|
|
64
|
+
* - Browser event handling (popstate, visibilitychange)
|
|
65
|
+
* - Accessible (ARIA labels, role="status")
|
|
66
|
+
* - RTL-safe styling
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* // Include once in app root (layout.tsx or App.tsx)
|
|
71
|
+
* import { RedirectOverlay } from '@donotdev/ui';
|
|
72
|
+
*
|
|
73
|
+
* function RootLayout({ children }) {
|
|
74
|
+
* return (
|
|
75
|
+
* <>
|
|
76
|
+
* {children}
|
|
77
|
+
* <RedirectOverlay />
|
|
78
|
+
* </>
|
|
79
|
+
* );
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* // Hooks trigger it automatically
|
|
83
|
+
* const checkout = useStripeBilling('checkout', authState);
|
|
84
|
+
* await checkout({ priceId: 'price_123' }); // Shows overlay automatically
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @version 0.0.1
|
|
88
|
+
* @since 0.0.1
|
|
89
|
+
* @author AMBROISE PARK Consulting
|
|
90
|
+
*/
|
|
91
|
+
export function RedirectOverlay() {
|
|
92
|
+
const { t } = useTranslation('dndev');
|
|
93
|
+
// Subscribe to overlay store
|
|
94
|
+
const isOpen = useOverlay('isRedirectOverlayOpen');
|
|
95
|
+
const operation = useOverlay('redirectOperation');
|
|
96
|
+
const phase = useOverlay('redirectPhase');
|
|
97
|
+
const showCancelButton = useOverlay('showCancelButton');
|
|
98
|
+
const config = useOverlay('redirectConfig');
|
|
99
|
+
const startTime = useOverlay('redirectStartTime');
|
|
100
|
+
// Actions
|
|
101
|
+
const setRedirectPhase = useOverlay('setRedirectPhase');
|
|
102
|
+
const setShowCancelButton = useOverlay('setShowCancelButton');
|
|
103
|
+
const hideRedirectOverlay = useOverlay('hideRedirectOverlay');
|
|
104
|
+
// Refs for cleanup
|
|
105
|
+
const phaseTimersRef = useRef([]);
|
|
106
|
+
const cancelTimerRef = useRef(null);
|
|
107
|
+
// Cancel handler
|
|
108
|
+
const handleCancel = useCallback(() => {
|
|
109
|
+
hideRedirectOverlay();
|
|
110
|
+
}, [hideRedirectOverlay]);
|
|
111
|
+
// Setup phase progression and cancel timer when overlay opens
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!isOpen || !startTime)
|
|
114
|
+
return;
|
|
115
|
+
const cancelTimeout = config?.cancelTimeout ?? DEFAULT_CANCEL_TIMEOUT;
|
|
116
|
+
const elapsed = Date.now() - startTime;
|
|
117
|
+
// Clear any existing timers
|
|
118
|
+
phaseTimersRef.current.forEach(clearTimeout);
|
|
119
|
+
phaseTimersRef.current = [];
|
|
120
|
+
if (cancelTimerRef.current) {
|
|
121
|
+
clearTimeout(cancelTimerRef.current);
|
|
122
|
+
cancelTimerRef.current = null;
|
|
123
|
+
}
|
|
124
|
+
// Schedule phase transitions
|
|
125
|
+
const schedulePhase = (targetPhase, delay) => {
|
|
126
|
+
const adjustedDelay = Math.max(0, delay - elapsed);
|
|
127
|
+
if (adjustedDelay > 0) {
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
setRedirectPhase(targetPhase);
|
|
130
|
+
}, adjustedDelay);
|
|
131
|
+
phaseTimersRef.current.push(timer);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Already past this phase
|
|
135
|
+
setRedirectPhase(targetPhase);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
schedulePhase('preparing', PHASE_TIMINGS.preparing);
|
|
139
|
+
schedulePhase('redirecting', PHASE_TIMINGS.redirecting);
|
|
140
|
+
// Schedule cancel button
|
|
141
|
+
const cancelDelay = Math.max(0, cancelTimeout - elapsed);
|
|
142
|
+
cancelTimerRef.current = setTimeout(() => {
|
|
143
|
+
setShowCancelButton(true);
|
|
144
|
+
setRedirectPhase('timeout');
|
|
145
|
+
}, cancelDelay);
|
|
146
|
+
// Cleanup
|
|
147
|
+
return () => {
|
|
148
|
+
phaseTimersRef.current.forEach(clearTimeout);
|
|
149
|
+
phaseTimersRef.current = [];
|
|
150
|
+
if (cancelTimerRef.current) {
|
|
151
|
+
clearTimeout(cancelTimerRef.current);
|
|
152
|
+
cancelTimerRef.current = null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}, [
|
|
156
|
+
isOpen,
|
|
157
|
+
startTime,
|
|
158
|
+
config?.cancelTimeout,
|
|
159
|
+
setRedirectPhase,
|
|
160
|
+
setShowCancelButton,
|
|
161
|
+
]);
|
|
162
|
+
// Handle browser back button (popstate)
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!isOpen)
|
|
165
|
+
return;
|
|
166
|
+
const handlePopState = () => {
|
|
167
|
+
// User pressed back - hide overlay
|
|
168
|
+
hideRedirectOverlay();
|
|
169
|
+
};
|
|
170
|
+
window.addEventListener('popstate', handlePopState);
|
|
171
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
172
|
+
}, [isOpen, hideRedirectOverlay]);
|
|
173
|
+
// Handle tab visibility change
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!isOpen)
|
|
176
|
+
return;
|
|
177
|
+
const handleVisibilityChange = () => {
|
|
178
|
+
// If user switches away and comes back, the redirect probably failed
|
|
179
|
+
// We could add logic here to show a "retry" state, but for now we keep overlay visible
|
|
180
|
+
// This prevents the overlay from disappearing when user tabs away briefly
|
|
181
|
+
};
|
|
182
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
183
|
+
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
184
|
+
}, [isOpen]);
|
|
185
|
+
// Don't render if not open
|
|
186
|
+
if (!isOpen)
|
|
187
|
+
return null;
|
|
188
|
+
// Get operation-specific translations
|
|
189
|
+
const operationKey = getOperationKey(operation);
|
|
190
|
+
// Try operation-specific translation, fallback to default
|
|
191
|
+
const title = config?.title ??
|
|
192
|
+
t(`redirectOverlay.${operationKey}.title`, {
|
|
193
|
+
defaultValue: t('redirectOverlay.default.title'),
|
|
194
|
+
});
|
|
195
|
+
const message = config?.message ??
|
|
196
|
+
t(`redirectOverlay.${operationKey}.message`, {
|
|
197
|
+
defaultValue: t('redirectOverlay.default.message'),
|
|
198
|
+
});
|
|
199
|
+
const subtitle = config?.subtitle ??
|
|
200
|
+
t(`redirectOverlay.${operationKey}.subtitle`, {
|
|
201
|
+
defaultValue: t('redirectOverlay.default.subtitle'),
|
|
202
|
+
});
|
|
203
|
+
const ariaLabel = t(`redirectOverlay.${operationKey}.ariaLabel`, {
|
|
204
|
+
defaultValue: t('redirectOverlay.default.ariaLabel'),
|
|
205
|
+
});
|
|
206
|
+
// Get phase message
|
|
207
|
+
const phaseMessage = t(`redirectOverlay.phases.${phase}`);
|
|
208
|
+
// Get icon
|
|
209
|
+
const IconComponent = getOperationIcon(operation, config?.icon);
|
|
210
|
+
return (_jsx(Portal, { children: _jsx("div", { className: "dndev-spinner-overlay", role: "status", "aria-busy": "true", "aria-label": ariaLabel, style: {
|
|
211
|
+
// Ensure highest z-index for redirect overlay
|
|
212
|
+
zIndex: 'var(--z-overlay, 9999)',
|
|
213
|
+
}, children: _jsxs("div", { style: {
|
|
214
|
+
display: 'flex',
|
|
215
|
+
flexDirection: 'column',
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
justifyContent: 'center',
|
|
218
|
+
gap: 'var(--gap-md)',
|
|
219
|
+
textAlign: 'center',
|
|
220
|
+
maxWidth: '400px',
|
|
221
|
+
padding: 'var(--gap-lg)',
|
|
222
|
+
}, children: [_jsxs(Stack, { direction: "row", gap: "medium", align: "center", children: [IconComponent && (_jsx(IconComponent, { style: {
|
|
223
|
+
width: '1.5rem',
|
|
224
|
+
height: '1.5rem',
|
|
225
|
+
color: 'var(--primary)',
|
|
226
|
+
}, "aria-hidden": "true" })), _jsx(Spinner, { variant: "primary" })] }), _jsx(Text, { as: "h3", style: {
|
|
227
|
+
color: 'var(--foreground)',
|
|
228
|
+
margin: 0,
|
|
229
|
+
}, children: title }), _jsx(Text, { variant: "muted", style: {
|
|
230
|
+
color: 'var(--foreground)',
|
|
231
|
+
minHeight: '1.5em',
|
|
232
|
+
}, children: phaseMessage }), _jsx(Text, { variant: "muted", level: "small", style: {
|
|
233
|
+
opacity: 0.8,
|
|
234
|
+
}, children: message }), _jsx(Text, { variant: "muted", level: "small", style: {
|
|
235
|
+
opacity: 0.6,
|
|
236
|
+
}, children: subtitle }), _jsx(Text, { variant: "muted", level: "small", style: {
|
|
237
|
+
opacity: 0.5,
|
|
238
|
+
marginTop: 'var(--gap-sm)',
|
|
239
|
+
}, children: t('redirectOverlay.doNotRefresh') }), showCancelButton && (_jsx(Button, { variant: "ghost", onClick: handleCancel, style: {
|
|
240
|
+
marginTop: 'var(--gap-md)',
|
|
241
|
+
}, children: t('redirectOverlay.cancel') }))] }) }) }));
|
|
242
|
+
}
|
|
243
|
+
export default RedirectOverlay;
|
|
@@ -13,6 +13,7 @@ export { default as Loader } from './Loader';
|
|
|
13
13
|
export { default as LoadingMessage, LoadingMessageSimple, LoadingMessageProcessing, LoadingMessageAlmostDone, LoadingMessageComplete, LoadingMessageSaving, LoadingMessageUploading, LoadingMessageSearching, LoadingMessageConnecting, } from './LoadingMessage';
|
|
14
14
|
export { default as LoadingOverlay, LoadingContainerOverlay, } from './LoadingOverlay';
|
|
15
15
|
export { default as LoadingScreen } from './LoadingScreen';
|
|
16
|
+
export { RedirectOverlay } from './RedirectOverlay';
|
|
16
17
|
export { default as ProgressBar } from './ProgressBar';
|
|
17
18
|
export { default as Skeleton } from './Skeleton';
|
|
18
19
|
export { default as FeatureCard } from './FeatureCard';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/common/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/common/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -14,6 +14,7 @@ export { default as Loader } from './Loader';
|
|
|
14
14
|
export { default as LoadingMessage, LoadingMessageSimple, LoadingMessageProcessing, LoadingMessageAlmostDone, LoadingMessageComplete, LoadingMessageSaving, LoadingMessageUploading, LoadingMessageSearching, LoadingMessageConnecting, } from './LoadingMessage';
|
|
15
15
|
export { default as LoadingOverlay, LoadingContainerOverlay, } from './LoadingOverlay';
|
|
16
16
|
export { default as LoadingScreen } from './LoadingScreen';
|
|
17
|
+
export { RedirectOverlay } from './RedirectOverlay';
|
|
17
18
|
export { default as ProgressBar } from './ProgressBar';
|
|
18
19
|
export { default as Skeleton } from './Skeleton';
|
|
19
20
|
export { default as FeatureCard } from './FeatureCard';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsMenu.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/components/header/SettingsMenu.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,aAAa,EAAwB,MAAM,sBAAsB,CAAC;AAYhF,MAAM,WAAW,iBAAiB;IAChC,qBAAqB;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;GAMG;AACH,iBAAS,YAAY,CAAC,EACpB,OAAgC,GACjC,GAAE,iBAAsB,
|
|
1
|
+
{"version":3,"file":"SettingsMenu.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/components/header/SettingsMenu.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,aAAa,EAAwB,MAAM,sBAAsB,CAAC;AAYhF,MAAM,WAAW,iBAAiB;IAChC,qBAAqB;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;GAMG;AACH,iBAAS,YAAY,CAAC,EACpB,OAAgC,GACjC,GAAE,iBAAsB,2CA+FxB;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -54,7 +54,7 @@ function SettingsMenu({ variant = BUTTON_VARIANT.OUTLINE, } = {}) {
|
|
|
54
54
|
checked: isActive,
|
|
55
55
|
className: isActive ? 'bg-accent' : undefined,
|
|
56
56
|
iconEnd: isActive && !isLoading ? Check : undefined,
|
|
57
|
-
children: (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Flag, { code: language.id, title: `${language.name} flag` }), _jsx("span", { children: language.name }), isLoading && isActive && (_jsx("div", { style: { marginInlineStart: 'auto' }, children: _jsx("div", { style: {
|
|
57
|
+
children: (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Flag, { code: language.flagCode || language.id, title: `${language.name} flag` }), _jsx("span", { children: language.name }), isLoading && isActive && (_jsx("div", { style: { marginInlineStart: 'auto' }, children: _jsx("div", { style: {
|
|
58
58
|
width: '0.5rem',
|
|
59
59
|
height: '0.5rem',
|
|
60
60
|
borderRadius: '50%',
|
|
@@ -94,7 +94,7 @@ export function DisplayFieldRenderer({ name, config, t, value, className, }) {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
// Don't render hidden fields
|
|
97
|
-
if (config.hidden) {
|
|
97
|
+
if (config.visibility === 'hidden') {
|
|
98
98
|
return _jsx(_Fragment, {});
|
|
99
99
|
}
|
|
100
100
|
const displayComponent = renderDisplayComponent();
|