@djangocfg/layouts 2.1.36 → 2.1.38

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.
Files changed (64) hide show
  1. package/README.md +204 -18
  2. package/package.json +5 -5
  3. package/src/components/errors/index.ts +9 -0
  4. package/src/components/errors/types.ts +38 -0
  5. package/src/layouts/AppLayout/AppLayout.tsx +33 -45
  6. package/src/layouts/AppLayout/BaseApp.tsx +105 -28
  7. package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
  8. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
  9. package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
  12. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  13. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  14. package/src/layouts/_components/UserMenu.tsx +1 -1
  15. package/src/layouts/index.ts +1 -1
  16. package/src/layouts/types/index.ts +47 -0
  17. package/src/layouts/types/layout.types.ts +61 -0
  18. package/src/layouts/types/providers.types.ts +65 -0
  19. package/src/layouts/types/ui.types.ts +103 -0
  20. package/src/snippets/Analytics/index.ts +1 -0
  21. package/src/snippets/Analytics/types.ts +10 -0
  22. package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
  23. package/src/snippets/PWAInstall/@docs/README.md +92 -0
  24. package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
  25. package/src/snippets/PWAInstall/README.md +185 -0
  26. package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
  27. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
  28. package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
  29. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
  30. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
  31. package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
  32. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
  33. package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
  34. package/src/snippets/PWAInstall/index.ts +76 -0
  35. package/src/snippets/PWAInstall/types/components.ts +95 -0
  36. package/src/snippets/PWAInstall/types/config.ts +22 -0
  37. package/src/snippets/PWAInstall/types/index.ts +26 -0
  38. package/src/snippets/PWAInstall/types/install.ts +38 -0
  39. package/src/snippets/PWAInstall/types/platform.ts +29 -0
  40. package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
  41. package/src/snippets/PWAInstall/utils/logger.ts +149 -0
  42. package/src/snippets/PWAInstall/utils/platform.ts +151 -0
  43. package/src/snippets/PushNotifications/@docs/README.md +191 -0
  44. package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
  45. package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
  46. package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
  47. package/src/snippets/PushNotifications/README.md +328 -0
  48. package/src/snippets/PushNotifications/components/PushPrompt.tsx +165 -0
  49. package/src/snippets/PushNotifications/config.ts +20 -0
  50. package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
  51. package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
  52. package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
  53. package/src/snippets/PushNotifications/index.ts +87 -0
  54. package/src/snippets/PushNotifications/types/config.ts +28 -0
  55. package/src/snippets/PushNotifications/types/index.ts +9 -0
  56. package/src/snippets/PushNotifications/types/push.ts +21 -0
  57. package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
  58. package/src/snippets/PushNotifications/utils/logger.ts +149 -0
  59. package/src/snippets/PushNotifications/utils/platform.ts +151 -0
  60. package/src/snippets/PushNotifications/utils/vapid.ts +226 -0
  61. package/src/snippets/index.ts +55 -0
  62. package/src/layouts/shared/index.ts +0 -21
  63. package/src/layouts/shared/types.ts +0 -211
  64. /package/src/layouts/{shared → types}/README.md +0 -0
@@ -0,0 +1,185 @@
1
+ # PWA Install
2
+
3
+ Progressive Web App installation snippet for web applications.
4
+
5
+ ## Responsibility
6
+
7
+ **Installation of PWA on user's device** (Add to Home Screen)
8
+
9
+ - iOS Safari: Visual guide for manual installation
10
+ - Android Chrome: Native install prompt
11
+ - Unified UX across platforms
12
+ - Smart dismissal with auto-reset
13
+
14
+ ## Quick Start
15
+
16
+ ```tsx
17
+ // app/layout.tsx
18
+ import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
19
+
20
+ export default function RootLayout({ children }) {
21
+ return (
22
+ <html>
23
+ <body>
24
+ <PwaProvider>
25
+ {children}
26
+ <A2HSHint resetAfterDays={3} />
27
+ </PwaProvider>
28
+ </body>
29
+ </html>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ## What You Get
35
+
36
+ - **Unified UX** → Same hint position (bottom) for both iOS & Android
37
+ - **iOS Safari** → Click hint → Opens visual step-by-step guide
38
+ - **Android Chrome** → Click hint → Native install prompt
39
+ - **Visual guide** → Adaptive (drawer on mobile, modal on desktop)
40
+ - **Zero config** → Works out of the box
41
+ - **Smart reset** → Re-appears after 3 days (user gets second chance)
42
+ - **No spam** → Dismissible, respects user choice
43
+
44
+ ## API
45
+
46
+ ### `<PwaProvider>`
47
+
48
+ Wrap your app once:
49
+
50
+ ```tsx
51
+ <PwaProvider>
52
+ {children}
53
+ </PwaProvider>
54
+ ```
55
+
56
+ ### `<A2HSHint />`
57
+
58
+ Unified install hint for iOS & Android (auto-shows, dismissible):
59
+
60
+ ```tsx
61
+ <A2HSHint
62
+ resetAfterDays={3} // Default: 3 days (set to null for never)
63
+ delayMs={3000} // Default: 3 seconds
64
+ forceShow={isDevelopment} // Show on ANY browser (for dev testing)
65
+ logo="/logo192.png" // App logo URL (fallback: Share icon)
66
+ />
67
+ ```
68
+
69
+ **Behavior (iOS Safari):**
70
+ - Shows after 3 seconds
71
+ - Text: "Keep terminal with you → Tap to learn how"
72
+ - Click → Opens visual guide (adaptive: drawer on mobile, modal on desktop)
73
+ - Dismissible (saved to localStorage)
74
+ - Auto-resets after 3 days
75
+
76
+ **Behavior (Android Chrome):**
77
+ - Shows after 3 seconds
78
+ - Text: "Install Cmdop → Tap to install"
79
+ - Click → Triggers native install prompt
80
+ - Dismissible (saved to localStorage)
81
+ - Auto-resets after 3 days
82
+
83
+ ### `useInstall()` hook
84
+
85
+ Access install state anywhere:
86
+
87
+ ```tsx
88
+ import { useInstall } from '@/snippets/PWAInstall';
89
+
90
+ function Header() {
91
+ const { isIOS, isInstalled, canPrompt, install } = useInstall();
92
+
93
+ return (
94
+ <header>
95
+ {canPrompt && <button onClick={install}>Install</button>}
96
+ {isInstalled && <span>✓ Installed</span>}
97
+ </header>
98
+ );
99
+ }
100
+ ```
101
+
102
+ **Returns:**
103
+ ```ts
104
+ {
105
+ isIOS: boolean;
106
+ isAndroid: boolean;
107
+ isSafari: boolean;
108
+ isChrome: boolean;
109
+ isInstalled: boolean;
110
+ canPrompt: boolean;
111
+ install: () => Promise<'accepted' | 'dismissed' | null>;
112
+ }
113
+ ```
114
+
115
+ ## Usage with Push Notifications
116
+
117
+ Use together with the PushNotifications snippet:
118
+
119
+ ```tsx
120
+ import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
121
+ import { DjangoPushProvider, PushPrompt } from '@/snippets/PushNotifications';
122
+
123
+ export default function Layout({ children }) {
124
+ return (
125
+ <PwaProvider>
126
+ <DjangoPushProvider vapidPublicKey={VAPID_KEY}>
127
+ {children}
128
+
129
+ {/* PWA Install hint */}
130
+ <A2HSHint resetAfterDays={3} />
131
+
132
+ {/* Push notification prompt (after PWA install) */}
133
+ <PushPrompt
134
+ requirePWA={true}
135
+ delayMs={5000}
136
+ resetAfterDays={7}
137
+ />
138
+ </DjangoPushProvider>
139
+ </PwaProvider>
140
+ );
141
+ }
142
+ ```
143
+
144
+ ## Browser Support
145
+
146
+ | Platform | Browser | Support |
147
+ |----------|---------|---------|
148
+ | iOS | Safari | ✅ Visual guide |
149
+ | iOS | Chrome/Firefox | ❌ No PWA support |
150
+ | Android | Chrome | ✅ Native prompt |
151
+ | Android | Firefox | âš ī¸ Manual only |
152
+ | Desktop | Chrome/Edge | ✅ Native prompt |
153
+
154
+ ## Architecture
155
+
156
+ ```
157
+ PWAInstall/
158
+ ├── context/
159
+ │ └── InstallContext.tsx # Install state management
160
+ ├── components/
161
+ │ ├── A2HSHint.tsx # Unified install hint
162
+ │ ├── IOSGuide.tsx # Visual guide wrapper
163
+ │ ├── IOSGuideDrawer.tsx # Mobile guide
164
+ │ └── IOSGuideModal.tsx # Desktop guide
165
+ ├── hooks/
166
+ │ ├── useInstallPrompt.ts # Install prompt logic
167
+ │ └── useIsPWA.ts # PWA detection
168
+ ├── utils/
169
+ │ ├── platform.ts # Platform detection
170
+ │ ├── localStorage.ts # Persistence
171
+ │ └── logger.ts # Logging
172
+ └── types/
173
+ ├── platform.ts
174
+ ├── install.ts
175
+ └── components.ts
176
+ ```
177
+
178
+ ## Separation from Push Notifications
179
+
180
+ This snippet is **completely independent** from push notifications:
181
+
182
+ - **PWAInstall** → Handles device installation
183
+ - **PushNotifications** → Handles web push subscriptions
184
+
185
+ Both can be used together or separately.
@@ -0,0 +1,227 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * PWA Install Hint (Unified for iOS & Android)
5
+ *
6
+ * Inline, non-blocking hint that shows at the bottom of the screen
7
+ * - iOS Safari: Opens visual guide on click
8
+ * - Android Chrome: Triggers native install prompt on click
9
+ * - Unified UX: Same position, same style, same behavior
10
+ *
11
+ * Auto-resets after 3 days (configurable)
12
+ */
13
+
14
+ import React, { useState, useEffect } from 'react';
15
+ import { Share, X, ChevronRight, Download } from 'lucide-react';
16
+ import { Button } from '@djangocfg/ui-nextjs';
17
+ import { cn } from '@djangocfg/ui-nextjs/lib';
18
+
19
+ import { useInstall } from '../context/InstallContext';
20
+ import { IOSGuide } from './IOSGuide';
21
+ import { DesktopGuide } from './DesktopGuide';
22
+ import { pwaLogger } from '../utils/logger';
23
+ import { markA2HSDismissed, isA2HSDismissedRecently } from '../utils/localStorage';
24
+
25
+ const DEFAULT_RESET_DAYS = 3;
26
+
27
+ interface A2HSHintProps {
28
+ /**
29
+ * Additional class names for the container
30
+ */
31
+ className?: string;
32
+
33
+ /**
34
+ * Number of days before re-showing dismissed hint
35
+ * @default 3
36
+ * Set to null to never reset (show once forever)
37
+ */
38
+ resetAfterDays?: number | null;
39
+
40
+ /**
41
+ * Delay before showing hint (ms)
42
+ * @default 3000
43
+ */
44
+ delayMs?: number;
45
+
46
+ /**
47
+ * Demo mode - shows hint on all platforms with appropriate guides
48
+ * Production: only iOS Safari & Android Chrome
49
+ * Demo: shows on desktop too with desktop install guide
50
+ * @default false
51
+ */
52
+ demo?: boolean;
53
+
54
+ /**
55
+ * App logo URL to display in hint
56
+ * If not provided, uses Share icon
57
+ */
58
+ logo?: string;
59
+ }
60
+
61
+ export function A2HSHint({
62
+ className,
63
+ resetAfterDays = DEFAULT_RESET_DAYS,
64
+ delayMs = 3000,
65
+ demo = false,
66
+ logo,
67
+ }: A2HSHintProps = {}) {
68
+ const { isIOS, isSafari, isAndroid, isDesktop, isInstalled, canPrompt, install } = useInstall();
69
+ const [show, setShow] = useState(false);
70
+ const [showGuide, setShowGuide] = useState(false);
71
+ const [installing, setInstalling] = useState(false);
72
+
73
+ // Determine if should show hint
74
+ // Production: only iOS Safari & Android Chrome with native prompt
75
+ // Demo: show on all platforms (desktop, iOS, Android)
76
+ const shouldShow = demo
77
+ ? !isInstalled // Demo: show on all platforms if not installed
78
+ : !isInstalled && ((isIOS && isSafari) || canPrompt); // Production: only supported platforms
79
+
80
+ useEffect(() => {
81
+ if (!shouldShow) return;
82
+
83
+ // Check if previously dismissed (skip localStorage check in demo mode)
84
+ if (!demo && typeof window !== 'undefined') {
85
+ // If resetAfterDays is null, never reset (check with very large number)
86
+ if (resetAfterDays === null) {
87
+ if (isA2HSDismissedRecently(Number.MAX_SAFE_INTEGER)) {
88
+ return; // Dismissed forever
89
+ }
90
+ } else if (isA2HSDismissedRecently(resetAfterDays)) {
91
+ return; // Still within reset period
92
+ }
93
+ }
94
+
95
+ // Show after delay (user is already engaged)
96
+ const timer = setTimeout(() => setShow(true), delayMs);
97
+ return () => clearTimeout(timer);
98
+ }, [shouldShow, resetAfterDays, delayMs, demo]);
99
+
100
+ const handleDismiss = () => {
101
+ setShow(false);
102
+ // Don't save to localStorage if demo mode (dev testing)
103
+ if (!demo) {
104
+ markA2HSDismissed();
105
+ }
106
+ };
107
+
108
+ const handleGuideDismiss = () => {
109
+ setShowGuide(false);
110
+ // In demo mode, keep hint visible after guide is dismissed
111
+ // In production mode, dismiss both guide and hint
112
+ if (!demo) {
113
+ handleDismiss();
114
+ }
115
+ };
116
+
117
+ const handleClick = async () => {
118
+ const isIOSPlatform = isIOS && isSafari;
119
+
120
+ // iOS or Desktop: Open visual guide
121
+ if (isIOSPlatform || isDesktop) {
122
+ setShowGuide(true);
123
+ } else if (canPrompt) {
124
+ // Android: Trigger native install prompt
125
+ setInstalling(true);
126
+ try {
127
+ await install();
128
+ // If install succeeds, dismiss hint
129
+ handleDismiss();
130
+ } catch (error) {
131
+ pwaLogger.error('[A2HSHint] Install error:', error);
132
+ } finally {
133
+ setInstalling(false);
134
+ }
135
+ }
136
+ };
137
+
138
+ if (!show) return null;
139
+
140
+ // Platform-specific content
141
+ const isIOSPlatform = isIOS && isSafari;
142
+
143
+ // Determine which guide/action to show
144
+ let title: string;
145
+ let subtitle: React.ReactNode;
146
+
147
+ if (isIOSPlatform) {
148
+ title = 'Add to Home Screen';
149
+ subtitle = <>Tap to learn how <ChevronRight className="w-3 h-3" /></>;
150
+ } else if (isDesktop) {
151
+ title = 'Install App';
152
+ subtitle = <>Click to see desktop guide <ChevronRight className="w-3 h-3" /></>;
153
+ } else {
154
+ // Android or other mobile with native prompt
155
+ title = 'Install App';
156
+ subtitle = <>Tap to install <Download className="w-3 h-3" /></>;
157
+ }
158
+
159
+ return (
160
+ <>
161
+ <div className={cn(
162
+ "fixed bottom-4 left-4 right-4 z-50 animate-in slide-in-from-bottom-4 duration-300",
163
+ demo && "relative inset-auto z-auto", // Demo mode: remove fixed positioning
164
+ className
165
+ )}>
166
+ {/* Make entire card clickable */}
167
+ <div
168
+ role="button"
169
+ tabIndex={0}
170
+ onClick={handleClick}
171
+ onKeyDown={(e) => {
172
+ if (e.key === 'Enter' || e.key === ' ') {
173
+ e.preventDefault();
174
+ handleClick();
175
+ }
176
+ }}
177
+ className={cn(
178
+ "w-full bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-lg cursor-pointer hover:bg-zinc-800 transition-colors",
179
+ installing && "opacity-70 cursor-not-allowed"
180
+ )}
181
+ aria-disabled={installing}
182
+ >
183
+ <div className="flex items-center gap-3">
184
+ {/* App logo or icon */}
185
+ <div className="flex-shrink-0">
186
+ {logo ? (
187
+ <img src={logo} alt="App logo" className="w-10 h-10 rounded-lg" />
188
+ ) : (
189
+ <Share className="w-5 h-5 text-blue-400" />
190
+ )}
191
+ </div>
192
+
193
+ {/* Content - now just displays, clicking anywhere triggers action */}
194
+ <div className="flex-1 min-w-0">
195
+ <p className="text-sm font-medium text-white mb-1">{title}</p>
196
+ <p className="text-xs text-zinc-400 flex items-center gap-1">
197
+ {installing ? 'Installing...' : subtitle}
198
+ </p>
199
+ </div>
200
+
201
+ {/* Close button - prevent event bubbling */}
202
+ <button
203
+ onClick={(e) => {
204
+ e.stopPropagation();
205
+ handleDismiss();
206
+ }}
207
+ className="flex-shrink-0 p-1 hover:bg-zinc-700 rounded transition-colors"
208
+ aria-label="Dismiss"
209
+ >
210
+ <X className="w-4 h-4 text-zinc-400" />
211
+ </button>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ {/* Show appropriate guide based on platform */}
217
+ {(isIOSPlatform || (demo && isIOS)) && (
218
+ <IOSGuide open={showGuide} onDismiss={handleGuideDismiss} />
219
+ )}
220
+
221
+ {/* Desktop guide */}
222
+ {isDesktop && (
223
+ <DesktopGuide open={showGuide} onDismiss={handleGuideDismiss} />
224
+ )}
225
+ </>
226
+ );
227
+ }
@@ -0,0 +1,229 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Desktop Installation Guide Modal
5
+ *
6
+ * Visual step-by-step guide for installing PWA on desktop browsers
7
+ * Uses platform detection from InstallContext
8
+ * Supports: Chrome, Edge, Brave, Arc, Vivaldi, Opera, Yandex, Firefox, Safari
9
+ */
10
+
11
+ import React, { useMemo } from 'react';
12
+ import { Monitor, Check, ArrowDownToLine, Menu, Search, Plus } from 'lucide-react';
13
+
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ Button,
22
+ Card,
23
+ CardContent,
24
+ } from '@djangocfg/ui-nextjs';
25
+
26
+ import type { IOSGuideModalProps, InstallStep } from '../types';
27
+ import { useInstall } from '../context/InstallContext';
28
+
29
+ type BrowserCategory = 'chromium' | 'firefox' | 'safari' | 'unknown';
30
+
31
+ function getBrowserCategory(browser: {
32
+ isChromium: boolean;
33
+ isFirefox: boolean;
34
+ isSafari: boolean;
35
+ }): BrowserCategory {
36
+ if (browser.isChromium) return 'chromium';
37
+ if (browser.isFirefox) return 'firefox';
38
+ if (browser.isSafari) return 'safari';
39
+ return 'unknown';
40
+ }
41
+
42
+ function getBrowserSteps(category: BrowserCategory, browserName: string): InstallStep[] {
43
+ switch (category) {
44
+ case 'chromium':
45
+ // Chrome, Edge, Brave, Arc, Vivaldi, Opera, Yandex, etc.
46
+ return [
47
+ {
48
+ number: 1,
49
+ title: 'Find Install Icon',
50
+ icon: ArrowDownToLine,
51
+ description: 'Look for install icon in address bar (right side)',
52
+ },
53
+ {
54
+ number: 2,
55
+ title: 'Click Install',
56
+ icon: Plus,
57
+ description: 'Click the icon and select "Install"',
58
+ },
59
+ {
60
+ number: 3,
61
+ title: 'Confirm',
62
+ icon: Check,
63
+ description: 'Click "Install" in the popup dialog',
64
+ },
65
+ ];
66
+
67
+ case 'firefox':
68
+ return [
69
+ {
70
+ number: 1,
71
+ title: 'Open Menu',
72
+ icon: Menu,
73
+ description: 'Click the menu button (three lines)',
74
+ },
75
+ {
76
+ number: 2,
77
+ title: 'Find Install Option',
78
+ icon: Search,
79
+ description: 'Look for "Install" or "Add to Home Screen"',
80
+ },
81
+ {
82
+ number: 3,
83
+ title: 'Confirm',
84
+ icon: Check,
85
+ description: 'Follow the installation prompts',
86
+ },
87
+ ];
88
+
89
+ case 'safari':
90
+ return [
91
+ {
92
+ number: 1,
93
+ title: 'Limited Support',
94
+ icon: Monitor,
95
+ description: 'Safari on macOS has limited PWA support',
96
+ },
97
+ {
98
+ number: 2,
99
+ title: 'Use Chromium Browser',
100
+ icon: ArrowDownToLine,
101
+ description: 'Consider using Chrome, Edge, or Brave for full PWA experience',
102
+ },
103
+ ];
104
+
105
+ default:
106
+ return [
107
+ {
108
+ number: 1,
109
+ title: 'Check Address Bar',
110
+ icon: ArrowDownToLine,
111
+ description: 'Look for an install or download icon',
112
+ },
113
+ {
114
+ number: 2,
115
+ title: 'Or Use Menu',
116
+ icon: Menu,
117
+ description: 'Check browser menu for install option',
118
+ },
119
+ {
120
+ number: 3,
121
+ title: 'Confirm',
122
+ icon: Check,
123
+ description: 'Follow the installation prompts',
124
+ },
125
+ ];
126
+ }
127
+ }
128
+
129
+ function StepCard({ step }: { step: InstallStep }) {
130
+ return (
131
+ <Card className="border border-border">
132
+ <CardContent className="p-4">
133
+ <div className="flex items-start gap-3">
134
+ <div
135
+ className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
136
+ style={{ width: '32px', height: '32px' }}
137
+ >
138
+ <span className="text-sm font-semibold">{step.number}</span>
139
+ </div>
140
+
141
+ <div className="flex-1 min-w-0">
142
+ <div className="flex items-center gap-2 mb-1">
143
+ <step.icon className="w-5 h-5 text-primary" />
144
+ <h3 className="font-semibold text-foreground">{step.title}</h3>
145
+ </div>
146
+ <p className="text-sm text-muted-foreground">{step.description}</p>
147
+ </div>
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+ );
152
+ }
153
+
154
+ export function DesktopGuide({ onDismiss, open = true }: IOSGuideModalProps) {
155
+ const {
156
+ browserName,
157
+ isChromium,
158
+ isFirefox,
159
+ isSafari,
160
+ isEdge,
161
+ isBrave,
162
+ isArc,
163
+ isVivaldi,
164
+ isOpera,
165
+ isYandex,
166
+ } = useInstall();
167
+
168
+ const category = useMemo(
169
+ () => getBrowserCategory({ isChromium, isFirefox, isSafari }),
170
+ [isChromium, isFirefox, isSafari]
171
+ );
172
+
173
+ const steps = useMemo(() => getBrowserSteps(category, browserName), [category, browserName]);
174
+
175
+ // Get specific browser display name with emoji
176
+ const displayName = useMemo(() => {
177
+ if (isEdge) return 'Edge';
178
+ if (isBrave) return 'Brave';
179
+ if (isArc) return 'Arc';
180
+ if (isVivaldi) return 'Vivaldi';
181
+ if (isOpera) return 'Opera';
182
+ if (isYandex) return 'Yandex Browser';
183
+ return browserName;
184
+ }, [browserName, isEdge, isBrave, isArc, isVivaldi, isOpera, isYandex]);
185
+
186
+ return (
187
+ <Dialog open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
188
+ <DialogContent className="sm:max-w-md">
189
+ <DialogHeader className="text-left">
190
+ <DialogTitle className="flex items-center gap-2">
191
+ <Monitor className="w-5 h-5 text-primary" />
192
+ Install App on Desktop
193
+ </DialogTitle>
194
+ <DialogDescription className="text-left">
195
+ {isSafari
196
+ ? 'Safari on macOS has limited PWA support. For the best experience, use Chrome, Edge, or Brave.'
197
+ : `Install this app on ${displayName} for quick access from your desktop`
198
+ }
199
+ </DialogDescription>
200
+ </DialogHeader>
201
+
202
+ <div className="space-y-3 py-4">
203
+ {steps.map((step) => (
204
+ <StepCard key={step.number} step={step} />
205
+ ))}
206
+ </div>
207
+
208
+ {category === 'chromium' && (
209
+ <div className="p-3 bg-muted/30 rounded-lg text-xs text-muted-foreground">
210
+ 💡 Tip: You can also right-click the page and look for "Install" option in {displayName}
211
+ </div>
212
+ )}
213
+
214
+ {category === 'firefox' && (
215
+ <div className="p-3 bg-amber-500/10 rounded-lg text-xs text-amber-700 dark:text-amber-400">
216
+ â„šī¸ Note: Firefox has limited PWA support. Some features may not work as expected.
217
+ </div>
218
+ )}
219
+
220
+ <DialogFooter>
221
+ <Button onClick={onDismiss} variant="default" className="w-full">
222
+ <Check className="w-4 h-4 mr-2" />
223
+ Got It
224
+ </Button>
225
+ </DialogFooter>
226
+ </DialogContent>
227
+ </Dialog>
228
+ );
229
+ }
@@ -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-nextjs/hooks';
14
+
15
+ import { IOSGuideModal } from './IOSGuideModal';
16
+ import { IOSGuideDrawer } from './IOSGuideDrawer';
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
+ }