@djangocfg/ui-nextjs 2.1.227 → 2.1.229

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.
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iOS Installation Guide (Adaptive)
5
+ *
6
+ * Automatically uses:
7
+ * - Drawer on mobile (better swipe UX)
8
+ * - Dialog on desktop/tablet
9
+ */
10
+
11
+ import React from 'react';
12
+
13
+ import { useIsMobile } from '@djangocfg/ui-core/hooks';
14
+
15
+ import { IOSGuideDrawer } from './IOSGuideDrawer';
16
+ import { IOSGuideModal } from './IOSGuideModal';
17
+
18
+ import type { IOSGuideModalProps } from '../types';
19
+
20
+ export function IOSGuide(props: IOSGuideModalProps) {
21
+ const isMobile = useIsMobile(); // Viewport < 768px
22
+
23
+ // Use drawer on mobile, dialog on desktop
24
+ if (isMobile) {
25
+ return <IOSGuideDrawer {...props} />;
26
+ }
27
+
28
+ return <IOSGuideModal {...props} />;
29
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iOS Installation Guide Drawer (Mobile-Optimized)
5
+ *
6
+ * Bottom drawer with swipe gestures for iOS installation guide
7
+ * Better UX for mobile devices
8
+ */
9
+
10
+ import { ArrowDown, ArrowUpRight, Check, CheckCircle, Share } from 'lucide-react';
11
+ import React, { useMemo } from 'react';
12
+
13
+ import { useAppT } from '@djangocfg/i18n';
14
+ import {
15
+ Button, Card, CardContent, Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle
16
+ } from '@djangocfg/ui-core/components';
17
+
18
+ import type { IOSGuideModalProps, InstallStep } from '../types';
19
+
20
+ function StepCard({ step }: { step: InstallStep }) {
21
+ return (
22
+ <Card className="border border-border">
23
+ <CardContent className="p-4">
24
+ <div className="flex items-start gap-3">
25
+ <div
26
+ className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
27
+ style={{ width: '32px', height: '32px' }}
28
+ >
29
+ <span className="text-sm font-semibold">{step.number}</span>
30
+ </div>
31
+
32
+ <div className="flex-1 min-w-0">
33
+ <div className="flex items-center gap-2 mb-1">
34
+ <step.icon className="w-5 h-5 text-primary" />
35
+ <h3 className="font-semibold text-foreground">{step.title}</h3>
36
+ </div>
37
+ <p className="text-sm text-muted-foreground">{step.description}</p>
38
+ </div>
39
+ </div>
40
+ </CardContent>
41
+ </Card>
42
+ );
43
+ }
44
+
45
+ export function IOSGuideDrawer({ onDismiss, open = true }: IOSGuideModalProps) {
46
+ const t = useAppT();
47
+
48
+ const labels = useMemo(() => ({
49
+ title: t('layouts.pwa.iosTitle'),
50
+ description: t('layouts.pwa.iosDescription'),
51
+ gotIt: t('layouts.pwa.gotIt'),
52
+ }), [t]);
53
+
54
+ const steps: InstallStep[] = useMemo(() => [
55
+ {
56
+ number: 1,
57
+ title: t('layouts.pwa.iosStep1Title'),
58
+ icon: ArrowUpRight,
59
+ description: t('layouts.pwa.iosStep1Desc'),
60
+ },
61
+ {
62
+ number: 2,
63
+ title: t('layouts.pwa.iosStep2Title'),
64
+ icon: ArrowDown,
65
+ description: t('layouts.pwa.iosStep2Desc'),
66
+ },
67
+ {
68
+ number: 3,
69
+ title: t('layouts.pwa.iosStep3Title'),
70
+ icon: CheckCircle,
71
+ description: t('layouts.pwa.iosStep3Desc'),
72
+ },
73
+ ], [t]);
74
+
75
+ return (
76
+ <Drawer open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
77
+ <DrawerContent>
78
+ <DrawerHeader className="text-left">
79
+ <DrawerTitle className="flex items-center gap-2">
80
+ <Share className="w-5 h-5 text-primary" />
81
+ {labels.title}
82
+ </DrawerTitle>
83
+ <DrawerDescription className="text-left">
84
+ {labels.description}
85
+ </DrawerDescription>
86
+ </DrawerHeader>
87
+
88
+ <div className="space-y-3 p-4">
89
+ {steps.map((step) => (
90
+ <StepCard key={step.number} step={step} />
91
+ ))}
92
+ </div>
93
+
94
+ <div className="p-4 pt-0">
95
+ <Button onClick={onDismiss} variant="default" className="w-full">
96
+ <Check className="w-4 h-4 mr-2" />
97
+ {labels.gotIt}
98
+ </Button>
99
+ </div>
100
+ </DrawerContent>
101
+ </Drawer>
102
+ );
103
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iOS Installation Guide Modal
5
+ *
6
+ * Visual step-by-step guide for adding PWA to home screen on iOS Safari
7
+ */
8
+
9
+ import { ArrowDown, ArrowUpRight, Check, CheckCircle, Share } from 'lucide-react';
10
+ import React, { useMemo } from 'react';
11
+
12
+ import { useAppT } from '@djangocfg/i18n';
13
+ import {
14
+ Button, Card, CardContent, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
15
+ DialogTitle
16
+ } from '@djangocfg/ui-core/components';
17
+
18
+ import type { IOSGuideModalProps, InstallStep } from '../types';
19
+
20
+ function StepCard({ step }: { step: InstallStep }) {
21
+ return (
22
+ <Card className="border border-border">
23
+ <CardContent className="p-4">
24
+ <div className="flex items-start gap-3">
25
+ <div
26
+ className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
27
+ style={{ width: '32px', height: '32px' }}
28
+ >
29
+ <span className="text-sm font-semibold">{step.number}</span>
30
+ </div>
31
+
32
+ <div className="flex-1 min-w-0">
33
+ <div className="flex items-center gap-2 mb-1">
34
+ <step.icon className="w-5 h-5 text-primary" />
35
+ <h3 className="font-semibold text-foreground">{step.title}</h3>
36
+ </div>
37
+ <p className="text-sm text-muted-foreground">{step.description}</p>
38
+ </div>
39
+ </div>
40
+ </CardContent>
41
+ </Card>
42
+ );
43
+ }
44
+
45
+ export function IOSGuideModal({ onDismiss, open = true }: IOSGuideModalProps) {
46
+ const t = useAppT();
47
+
48
+ const labels = useMemo(() => ({
49
+ title: t('layouts.pwa.iosTitle'),
50
+ description: t('layouts.pwa.iosDescription'),
51
+ gotIt: t('layouts.pwa.gotIt'),
52
+ }), [t]);
53
+
54
+ const steps: InstallStep[] = useMemo(() => [
55
+ {
56
+ number: 1,
57
+ title: t('layouts.pwa.iosStep1Title'),
58
+ icon: ArrowUpRight,
59
+ description: t('layouts.pwa.iosStep1Desc'),
60
+ },
61
+ {
62
+ number: 2,
63
+ title: t('layouts.pwa.iosStep2Title'),
64
+ icon: ArrowDown,
65
+ description: t('layouts.pwa.iosStep2Desc'),
66
+ },
67
+ {
68
+ number: 3,
69
+ title: t('layouts.pwa.iosStep3Title'),
70
+ icon: CheckCircle,
71
+ description: t('layouts.pwa.iosStep3Desc'),
72
+ },
73
+ ], [t]);
74
+
75
+ return (
76
+ <Dialog open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
77
+ <DialogContent className="sm:max-w-md">
78
+ <DialogHeader className="text-left">
79
+ <DialogTitle className="flex items-center gap-2">
80
+ <Share className="w-5 h-5 text-primary" />
81
+ {labels.title}
82
+ </DialogTitle>
83
+ <DialogDescription className="text-left">
84
+ {labels.description}
85
+ </DialogDescription>
86
+ </DialogHeader>
87
+
88
+ <div className="space-y-3 py-4">
89
+ {steps.map((step) => (
90
+ <StepCard key={step.number} step={step} />
91
+ ))}
92
+ </div>
93
+
94
+ <DialogFooter>
95
+ <Button onClick={onDismiss} variant="default" className="w-full">
96
+ <Check className="w-4 h-4 mr-2" />
97
+ {labels.gotIt}
98
+ </Button>
99
+ </DialogFooter>
100
+ </DialogContent>
101
+ </Dialog>
102
+ );
103
+ }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * PWA Page Resume Manager
5
+ *
6
+ * Invisible component that manages page resume functionality for PWA.
7
+ * Add this component to your app layout to enable page resume on PWA launch.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // In your layout
12
+ * <PWAPageResumeManager enabled={true} />
13
+ * ```
14
+ */
15
+
16
+ import { usePWAPageResume } from '../hooks/usePWAPageResume';
17
+
18
+ interface PWAPageResumeManagerProps {
19
+ /**
20
+ * Enable page resume feature
21
+ * @default true
22
+ */
23
+ enabled?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Component that manages PWA page resume functionality
28
+ * Renders nothing, just manages state and navigation
29
+ */
30
+ export function PWAPageResumeManager({ enabled = true }: PWAPageResumeManagerProps) {
31
+ usePWAPageResume({ enabled });
32
+ return null; // Renders nothing, just manages state
33
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * PWA Install Context
5
+ *
6
+ * Minimal global state for PWA installation
7
+ * No tracking, no metrics, no engagement — just install state
8
+ */
9
+
10
+ import React, { createContext, ReactNode, useContext } from 'react';
11
+
12
+ import { useInstallPrompt } from '../hooks/useInstallPrompt';
13
+
14
+ import type { InstallOutcome } from '../types';
15
+ export interface PwaContextValue {
16
+ // Platform
17
+ isIOS: boolean;
18
+ isAndroid: boolean;
19
+ isDesktop: boolean;
20
+
21
+ // Browsers
22
+ isSafari: boolean;
23
+ isChrome: boolean;
24
+ isFirefox: boolean;
25
+ isEdge: boolean;
26
+ isOpera: boolean;
27
+ isBrave: boolean;
28
+ isArc: boolean;
29
+ isVivaldi: boolean;
30
+ isYandex: boolean;
31
+ isSamsungBrowser: boolean;
32
+ isUCBrowser: boolean;
33
+ isChromium: boolean; // Any Chromium-based browser
34
+ browserName: string;
35
+
36
+ // State
37
+ isInstalled: boolean;
38
+ canPrompt: boolean;
39
+
40
+ // Actions
41
+ install: () => Promise<InstallOutcome>;
42
+ }
43
+
44
+ const PwaContext = createContext<PwaContextValue | undefined>(undefined);
45
+
46
+ export interface PwaConfig {
47
+ enabled?: boolean;
48
+ }
49
+
50
+ export function PwaProvider({ children, ...config }: PwaConfig & { children: ReactNode }) {
51
+ // If not enabled, acts as a simple pass-through
52
+ if (config.enabled === false) {
53
+ return <>{children}</>;
54
+ }
55
+
56
+ const prompt = useInstallPrompt();
57
+
58
+ const value: PwaContextValue = {
59
+ // Platform
60
+ isIOS: prompt.isIOS,
61
+ isAndroid: prompt.isAndroid,
62
+ isDesktop: !prompt.isIOS && !prompt.isAndroid,
63
+
64
+ // Browsers (from useBrowserDetect)
65
+ isSafari: prompt.browser.isSafari && !prompt.browser.isChromium, // Real Safari only
66
+ isChrome: prompt.browser.isChrome,
67
+ isFirefox: prompt.browser.isFirefox,
68
+ isEdge: prompt.browser.isEdge,
69
+ isOpera: prompt.browser.isOpera,
70
+ isBrave: prompt.browser.isBrave,
71
+ isArc: prompt.browser.isArc,
72
+ isVivaldi: prompt.browser.isVivaldi,
73
+ isYandex: prompt.browser.isYandex,
74
+ isSamsungBrowser: prompt.browser.isSamsungBrowser,
75
+ isUCBrowser: prompt.browser.isUCBrowser,
76
+ isChromium: prompt.browser.isChromium,
77
+ browserName: prompt.browser.browserName,
78
+
79
+ // State
80
+ isInstalled: prompt.isInstalled,
81
+ canPrompt: prompt.canPrompt,
82
+
83
+ // Actions
84
+ install: prompt.promptInstall,
85
+ };
86
+
87
+ return <PwaContext.Provider value={value}>{children}</PwaContext.Provider>;
88
+ }
89
+
90
+ /**
91
+ * Use install context
92
+ * Must be used within <PwaProvider>
93
+ */
94
+ export function useInstall(): PwaContextValue {
95
+ const context = useContext(PwaContext);
96
+
97
+ if (context === undefined) {
98
+ throw new Error('useInstall must be used within <PwaProvider>');
99
+ }
100
+
101
+ return context;
102
+ }
@@ -0,0 +1,168 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * PWA Install Prompt Hook
5
+ *
6
+ * Manages beforeinstallprompt event and installation state
7
+ * Uses existing hooks from @djangocfg/ui-core for platform detection
8
+ */
9
+
10
+ import { useEffect, useState } from 'react';
11
+
12
+ import { useBrowserDetect, useDeviceDetect } from '../../hooks';
13
+
14
+ import { markAppInstalled } from '../utils/localStorage';
15
+ import { pwaLogger } from '../utils/logger';
16
+ import { isStandalone, onDisplayModeChange } from '../utils/platform';
17
+
18
+ import type { BeforeInstallPromptEvent, InstallPromptState, InstallOutcome } from '../types';
19
+ export function useInstallPrompt() {
20
+ const browser = useBrowserDetect();
21
+ const device = useDeviceDetect();
22
+
23
+ const [state, setState] = useState<InstallPromptState>(() => {
24
+ if (typeof window === 'undefined') {
25
+ return {
26
+ isIOS: false,
27
+ isAndroid: false,
28
+ isSafari: false,
29
+ isChrome: false,
30
+ isInstalled: false,
31
+ canPrompt: false,
32
+ deferredPrompt: null,
33
+ };
34
+ }
35
+
36
+ // Real Safari = Safari && NOT Chromium (avoid Arc, Brave on iOS)
37
+ const isSafari = browser.isSafari && !browser.isChromium;
38
+
39
+ return {
40
+ isIOS: device.isIOS,
41
+ isAndroid: device.isAndroid,
42
+ isSafari,
43
+ isChrome: browser.isChrome || browser.isChromium,
44
+ isInstalled: isStandalone(),
45
+ canPrompt: false,
46
+ deferredPrompt: null,
47
+ };
48
+ });
49
+
50
+ // Update state when platform info changes
51
+ useEffect(() => {
52
+ const isSafari = browser.isSafari && !browser.isChromium;
53
+
54
+ setState((prev) => ({
55
+ ...prev,
56
+ isIOS: device.isIOS,
57
+ isAndroid: device.isAndroid,
58
+ isSafari,
59
+ isChrome: browser.isChrome || browser.isChromium,
60
+ isInstalled: isStandalone(),
61
+ }));
62
+ }, [browser, device]);
63
+
64
+ // Listen for beforeinstallprompt event (Android Chrome only)
65
+ useEffect(() => {
66
+ if (typeof window === 'undefined') return;
67
+
68
+ const handleBeforeInstallPrompt = (e: Event) => {
69
+ // Prevent the default browser install prompt
70
+ e.preventDefault();
71
+
72
+ const event = e as BeforeInstallPromptEvent;
73
+
74
+ setState((prev) => ({
75
+ ...prev,
76
+ canPrompt: true,
77
+ deferredPrompt: event,
78
+ }));
79
+ };
80
+
81
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
82
+
83
+ return () => {
84
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
85
+ };
86
+ }, []);
87
+
88
+ // Listen for appinstalled event (Android Chrome)
89
+ useEffect(() => {
90
+ if (typeof window === 'undefined') return;
91
+
92
+ const handleAppInstalled = () => {
93
+ setState((prev) => ({
94
+ ...prev,
95
+ canPrompt: false,
96
+ deferredPrompt: null,
97
+ isInstalled: true,
98
+ }));
99
+
100
+ // Mark as installed in localStorage
101
+ markAppInstalled();
102
+ };
103
+
104
+ window.addEventListener('appinstalled', handleAppInstalled);
105
+
106
+ return () => {
107
+ window.removeEventListener('appinstalled', handleAppInstalled);
108
+ };
109
+ }, []);
110
+
111
+ // Check for display-mode changes (user adds to home screen)
112
+ useEffect(() => {
113
+ const cleanup = onDisplayModeChange((isStandaloneMode) => {
114
+ if (isStandaloneMode) {
115
+ setState((prev) => ({
116
+ ...prev,
117
+ isInstalled: true,
118
+ canPrompt: false,
119
+ deferredPrompt: null,
120
+ }));
121
+
122
+ markAppInstalled();
123
+ }
124
+ });
125
+
126
+ return cleanup;
127
+ }, []);
128
+
129
+ /**
130
+ * Trigger Android native install prompt
131
+ */
132
+ const promptInstall = async (): Promise<InstallOutcome> => {
133
+ if (!state.deferredPrompt) {
134
+ pwaLogger.warn('[PWA Install] No deferred prompt available');
135
+ return null;
136
+ }
137
+
138
+ try {
139
+ // Show the native prompt
140
+ await state.deferredPrompt.prompt();
141
+
142
+ // Wait for user response
143
+ const { outcome } = await state.deferredPrompt.userChoice;
144
+
145
+ pwaLogger.info('[PWA Install] User choice:', outcome);
146
+
147
+ // Clear the deferred prompt
148
+ setState((prev) => ({
149
+ ...prev,
150
+ deferredPrompt: null,
151
+ canPrompt: false,
152
+ }));
153
+
154
+ return outcome;
155
+ } catch (error) {
156
+ pwaLogger.error('[PWA Install] Error showing install prompt:', error);
157
+ return null;
158
+ }
159
+ };
160
+
161
+ return {
162
+ ...state,
163
+ promptInstall,
164
+ // Expose full browser info
165
+ browser,
166
+ device,
167
+ };
168
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Hook to detect if app is running as PWA
5
+ *
6
+ * Checks if the app is running in standalone mode (added to home screen).
7
+ * Results are cached in sessionStorage for fast initialization across page loads.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Basic usage (standard check)
12
+ * const isPWA = useIsPWA();
13
+ *
14
+ * // Reliable check (with manifest validation for desktop)
15
+ * const isPWA = useIsPWA({ reliable: true });
16
+ * ```
17
+ */
18
+
19
+ import { useEffect, useState } from 'react';
20
+
21
+ import { isStandalone, isStandaloneReliable, onDisplayModeChange } from '../utils/platform';
22
+
23
+ const CACHE_KEY = 'pwa_is_standalone';
24
+
25
+ /**
26
+ * Options for useIsPWA hook
27
+ */
28
+ export interface UseIsPWAOptions {
29
+ /**
30
+ * Use reliable check with additional validation for desktop browsers
31
+ * This prevents false positives on Safari macOS "Add to Dock"
32
+ * @default false
33
+ */
34
+ reliable?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Hook to detect if app is running as PWA (standalone mode)
39
+ *
40
+ * @param options - Configuration options
41
+ * @returns true if app is running as PWA
42
+ */
43
+ export function useIsPWA(options?: UseIsPWAOptions): boolean {
44
+ const checkFunction = options?.reliable ? isStandaloneReliable : isStandalone;
45
+
46
+ const [isPWA, setIsPWA] = useState<boolean>(() => {
47
+ // Try to restore from cache for fast initialization
48
+ if (typeof window !== 'undefined') {
49
+ try {
50
+ const cached = sessionStorage.getItem(CACHE_KEY);
51
+ if (cached !== null) {
52
+ return cached === 'true';
53
+ }
54
+ } catch {
55
+ // Ignore sessionStorage errors (e.g., in incognito mode)
56
+ }
57
+ }
58
+
59
+ // Initial check
60
+ return checkFunction();
61
+ });
62
+
63
+ useEffect(() => {
64
+ // Check after mount (may differ from SSR)
65
+ const isStandaloneMode = checkFunction();
66
+ setIsPWA(isStandaloneMode);
67
+
68
+ // Update cache
69
+ if (typeof window !== 'undefined') {
70
+ try {
71
+ sessionStorage.setItem(CACHE_KEY, String(isStandaloneMode));
72
+ } catch {
73
+ // Ignore sessionStorage errors
74
+ }
75
+ }
76
+
77
+ // Listen for display-mode changes
78
+ const cleanup = onDisplayModeChange((newValue) => {
79
+ setIsPWA(newValue);
80
+
81
+ // Update cache
82
+ if (typeof window !== 'undefined') {
83
+ try {
84
+ sessionStorage.setItem(CACHE_KEY, String(newValue));
85
+ } catch {
86
+ // Ignore sessionStorage errors
87
+ }
88
+ }
89
+ });
90
+
91
+ return cleanup;
92
+ }, [checkFunction]);
93
+
94
+ return isPWA;
95
+ }
96
+
97
+ /**
98
+ * Clear isPWA cache
99
+ *
100
+ * Useful for testing or when you want to force a re-check
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * import { clearIsPWACache } from '@djangocfg/layouts/snippets';
105
+ * clearIsPWACache();
106
+ * window.location.reload();
107
+ * ```
108
+ */
109
+ export function clearIsPWACache(): void {
110
+ if (typeof window === 'undefined') return;
111
+ try {
112
+ sessionStorage.removeItem(CACHE_KEY);
113
+ } catch {
114
+ // Ignore sessionStorage errors
115
+ }
116
+ }