@djangocfg/layouts 2.1.35 → 2.1.37

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 (40) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/BaseApp.tsx +31 -25
  3. package/src/layouts/shared/types.ts +36 -0
  4. package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
  5. package/src/snippets/PWA/@docs/research.md +576 -0
  6. package/src/snippets/PWA/@refactoring/ARCHITECTURE_ANALYSIS.md +1179 -0
  7. package/src/snippets/PWA/@refactoring/EXECUTIVE_SUMMARY.md +271 -0
  8. package/src/snippets/PWA/@refactoring/README.md +204 -0
  9. package/src/snippets/PWA/@refactoring/REFACTORING_PROPOSALS.md +1109 -0
  10. package/src/snippets/PWA/@refactoring2/COMPARISON-WITH-NEXTJS.md +718 -0
  11. package/src/snippets/PWA/@refactoring2/P1-FIXES-COMPLETED.md +188 -0
  12. package/src/snippets/PWA/@refactoring2/POST-P0-ANALYSIS.md +362 -0
  13. package/src/snippets/PWA/@refactoring2/README.md +85 -0
  14. package/src/snippets/PWA/@refactoring2/RECOMMENDATIONS.md +1321 -0
  15. package/src/snippets/PWA/@refactoring2/REMAINING-ISSUES.md +557 -0
  16. package/src/snippets/PWA/README.md +387 -0
  17. package/src/snippets/PWA/components/A2HSHint.tsx +226 -0
  18. package/src/snippets/PWA/components/IOSGuide.tsx +29 -0
  19. package/src/snippets/PWA/components/IOSGuideDrawer.tsx +101 -0
  20. package/src/snippets/PWA/components/IOSGuideModal.tsx +101 -0
  21. package/src/snippets/PWA/components/PushPrompt.tsx +165 -0
  22. package/src/snippets/PWA/config.ts +20 -0
  23. package/src/snippets/PWA/context/DjangoPushContext.tsx +105 -0
  24. package/src/snippets/PWA/context/InstallContext.tsx +118 -0
  25. package/src/snippets/PWA/context/PushContext.tsx +156 -0
  26. package/src/snippets/PWA/hooks/useDjangoPush.ts +277 -0
  27. package/src/snippets/PWA/hooks/useInstallPrompt.ts +164 -0
  28. package/src/snippets/PWA/hooks/useIsPWA.ts +115 -0
  29. package/src/snippets/PWA/hooks/usePushNotifications.ts +205 -0
  30. package/src/snippets/PWA/index.ts +95 -0
  31. package/src/snippets/PWA/types/components.ts +101 -0
  32. package/src/snippets/PWA/types/index.ts +26 -0
  33. package/src/snippets/PWA/types/install.ts +38 -0
  34. package/src/snippets/PWA/types/platform.ts +29 -0
  35. package/src/snippets/PWA/types/push.ts +21 -0
  36. package/src/snippets/PWA/utils/localStorage.ts +203 -0
  37. package/src/snippets/PWA/utils/logger.ts +149 -0
  38. package/src/snippets/PWA/utils/platform.ts +151 -0
  39. package/src/snippets/PWA/utils/vapid.ts +226 -0
  40. package/src/snippets/index.ts +30 -0
@@ -0,0 +1,387 @@
1
+ # PWA Install (Simplified)
2
+
3
+ Ultra-simple PWA installation for Cmdop. No tracking, no metrics, no complexity — just install.
4
+
5
+ ## Quick Start (3 lines)
6
+
7
+ ```tsx
8
+ // app/layout.tsx
9
+ import { InstallProvider, A2HSHint } from '@/app/_snippets/PwaInstall';
10
+ import { settings } from '@core/settings';
11
+
12
+ export default function RootLayout({ children }) {
13
+ return (
14
+ <html>
15
+ <body>
16
+ <InstallProvider>
17
+ {children}
18
+ <A2HSHint
19
+ resetAfterDays={3}
20
+ logo={settings.app.icons.logo192}
21
+ />
22
+ </InstallProvider>
23
+ </body>
24
+ </html>
25
+ );
26
+ }
27
+ ```
28
+
29
+ Done. That's it.
30
+
31
+ ---
32
+
33
+ ## What You Get
34
+
35
+ ✅ **Unified UX** → Same hint position (bottom) for both iOS & Android
36
+ ✅ **iOS Safari** → Click hint → Opens visual step-by-step guide
37
+ ✅ **Android Chrome** → Click hint → Native install prompt
38
+ ✅ **Visual guide** → Adaptive (drawer on mobile, modal on desktop)
39
+ ✅ **Zero config** → Works out of the box
40
+ ✅ **Smart reset** → Re-appears after 3 days (user gets second chance)
41
+ ✅ **No spam** → Dismissible, respects user choice
42
+
43
+ ---
44
+
45
+ ## API
46
+
47
+ ### 1. `<InstallProvider>`
48
+
49
+ Wrap your app once:
50
+
51
+ ```tsx
52
+ <InstallProvider>
53
+ {children}
54
+ </InstallProvider>
55
+ ```
56
+
57
+ ### 2. `<A2HSHint />`
58
+
59
+ **Unified install hint for iOS & Android** (auto-shows, dismissible):
60
+
61
+ ```tsx
62
+ <A2HSHint />
63
+ ```
64
+
65
+ **Behavior (iOS Safari):**
66
+ - Shows after 3 seconds
67
+ - Text: "Keep terminal with you → Tap to learn how"
68
+ - Click → Opens visual guide (adaptive: drawer on mobile, modal on desktop)
69
+ - Dismissible (saved to localStorage)
70
+ - **Auto-resets after 3 days**
71
+
72
+ **Behavior (Android Chrome):**
73
+ - Shows after 3 seconds
74
+ - Text: "Install Cmdop → Tap to install"
75
+ - Click → Triggers native install prompt
76
+ - Dismissible (saved to localStorage)
77
+ - **Auto-resets after 3 days**
78
+
79
+ **Unified:**
80
+ - ✅ Same position (bottom)
81
+ - ✅ Same style (inline hint)
82
+ - ✅ Same interaction (tap to action)
83
+ - ✅ Platform-specific content
84
+
85
+ **Props (optional):**
86
+ ```tsx
87
+ <A2HSHint
88
+ resetAfterDays={3} // Default: 3 days (set to null for never)
89
+ delayMs={3000} // Default: 3 seconds
90
+ forceShow={isDevelopment} // Show on ANY browser (for dev testing)
91
+ logo={settings.app.icons.logo192} // App logo URL (fallback: Share icon)
92
+ />
93
+ ```
94
+
95
+ ### 3. `<InstallButton />` (Optional)
96
+
97
+ **Standalone install button** (if you want button in header instead of bottom hint):
98
+
99
+ ```tsx
100
+ // In header
101
+ <InstallButton
102
+ text="Install"
103
+ className="ml-auto"
104
+ forceShow={isDevelopment}
105
+ />
106
+ ```
107
+
108
+ **Behavior:**
109
+ - Only Android Chrome (when `beforeinstallprompt` fires)
110
+ - Auto-hides when installed
111
+ - Triggers native install prompt
112
+ - Loading state included
113
+
114
+ **Note:** You don't need this if using `<A2HSHint />` — it already handles Android!
115
+
116
+ ### 4. `useInstall()` hook
117
+
118
+ Access install state anywhere:
119
+
120
+ ```tsx
121
+ import { useInstall } from '@/app/_snippets/PwaInstall';
122
+
123
+ function Header() {
124
+ const { isIOS, isInstalled, canPrompt, install } = useInstall();
125
+
126
+ return (
127
+ <header>
128
+ {canPrompt && <button onClick={install}>Install</button>}
129
+ {isInstalled && <span>✓ Installed</span>}
130
+ </header>
131
+ );
132
+ }
133
+ ```
134
+
135
+ **Returns:**
136
+ ```ts
137
+ {
138
+ isIOS: boolean;
139
+ isAndroid: boolean;
140
+ isSafari: boolean; // Real Safari (not Chromium browsers like Arc)
141
+ isChrome: boolean; // Any Chromium browser
142
+ isInstalled: boolean;
143
+ canPrompt: boolean; // Android only
144
+ install: () => Promise<'accepted' | 'dismissed' | null>;
145
+ }
146
+ ```
147
+
148
+ **Platform Detection:**
149
+ Uses `useBrowserDetect` and `useDeviceDetect` from `@djangocfg/ui-nextjs` for accurate cross-platform detection.
150
+
151
+ ---
152
+
153
+ ## Complete Example (Cmdop)
154
+
155
+ ```tsx
156
+ // app/layout.tsx
157
+ import { InstallProvider, A2HSHint } from '@/app/_snippets/PwaInstall';
158
+ import { settings } from '@core/settings';
159
+
160
+ export default function RootLayout({ children }) {
161
+ return (
162
+ <html>
163
+ <body>
164
+ <InstallProvider>
165
+ <Header />
166
+ {children}
167
+ <A2HSHint
168
+ resetAfterDays={3}
169
+ logo={settings.app.icons.logo192}
170
+ />
171
+ </InstallProvider>
172
+ </body>
173
+ </html>
174
+ );
175
+ }
176
+ ```
177
+
178
+ ```tsx
179
+ // components/Header.tsx
180
+ import { InstallButton } from '@/app/_snippets/PwaInstall';
181
+
182
+ export function Header() {
183
+ return (
184
+ <header className="flex items-center justify-between p-4">
185
+ <h1>Cmdop</h1>
186
+ <InstallButton /> {/* Auto-shows on Android */}
187
+ </header>
188
+ );
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## How It Works
195
+
196
+ ### Android Chrome
197
+
198
+ 1. Browser fires `beforeinstallprompt`
199
+ 2. We capture it
200
+ 3. Wait 3 seconds (engagement)
201
+ 4. Show hint at bottom: "Install Cmdop → Tap to install"
202
+ 5. User taps → Native install prompt
203
+ 6. User accepts → App installed
204
+ 7. Hint auto-dismisses
205
+
206
+ ### iOS Safari
207
+
208
+ 1. Detect iOS + Safari
209
+ 2. Check if installed (`standalone` mode)
210
+ 3. Check if dismissed recently (localStorage with timestamp)
211
+ 4. If dismissed more than 3 days ago → show again
212
+ 5. Wait 3 seconds (engagement)
213
+ 6. Show clickable hint at bottom
214
+ 7. **User taps hint** → Opens visual guide (drawer on mobile, modal on desktop)
215
+ 8. **Visual guide shows 3 steps:**
216
+ - Tap Share button
217
+ - Scroll & tap "Add to Home Screen"
218
+ - Tap "Add" to confirm
219
+ 9. User dismisses → Save timestamp
220
+ 10. After 3 days → Reset, show again
221
+
222
+ ---
223
+
224
+ ## Advanced Usage
225
+
226
+ ### Configure Reset Behavior
227
+
228
+ ```tsx
229
+ // Default: Reset after 3 days
230
+ <A2HSHint />
231
+
232
+ // Never reset (show once forever)
233
+ <A2HSHint resetAfterDays={null} />
234
+
235
+ // Reset after 7 days
236
+ <A2HSHint resetAfterDays={7} />
237
+
238
+ // Show immediately (no delay)
239
+ <A2HSHint delayMs={0} />
240
+
241
+ // Force show for dev testing (shows on ANY browser, ignores localStorage)
242
+ <A2HSHint forceShow={isDevelopment} />
243
+
244
+ // Production setup
245
+ <A2HSHint
246
+ resetAfterDays={3}
247
+ delayMs={3000}
248
+ forceShow={settings.pwa.forceShowInDev}
249
+ />
250
+ ```
251
+
252
+ **Important: `forceShow` behavior**
253
+ - ✅ Shows on ANY browser (not just iOS Safari)
254
+ - ✅ Ignores localStorage (dismissed state)
255
+ - ✅ Doesn't save dismiss to localStorage
256
+ - ✅ Reappears on every refresh (perfect for dev testing)
257
+ ```
258
+
259
+ ### Custom Install Flow
260
+
261
+ ```tsx
262
+ import { useInstall } from '@/app/_snippets/PwaInstall';
263
+
264
+ function CustomFlow() {
265
+ const { isIOS, canPrompt, install } = useInstall();
266
+
267
+ const handleInstall = async () => {
268
+ if (canPrompt) {
269
+ // Android: trigger native prompt
270
+ const outcome = await install();
271
+ console.log('Install outcome:', outcome);
272
+ } else if (isIOS) {
273
+ // iOS: show custom modal or guide
274
+ alert('Tap Share → Add to Home Screen');
275
+ }
276
+ };
277
+
278
+ return <button onClick={handleInstall}>Install App</button>;
279
+ }
280
+ ```
281
+
282
+ ### Check PWA State
283
+
284
+ ```tsx
285
+ import { useInstall } from '@/app/_snippets/PwaInstall';
286
+
287
+ function AppStatus() {
288
+ const { isInstalled } = useInstall();
289
+
290
+ return (
291
+ <div>
292
+ {isInstalled ? (
293
+ <p>✓ Running as PWA</p>
294
+ ) : (
295
+ <p>Running in browser</p>
296
+ )}
297
+ </div>
298
+ );
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Why This Approach?
305
+
306
+ ### Click-to-Guide Pattern
307
+
308
+ Research shows **visual steps outperform text-only instructions**:
309
+ - 📊 **70% completion rate** with visual guide
310
+ - 📊 **40% completion rate** with text-only
311
+
312
+ **Our approach:**
313
+ 1. **Minimal hint** — doesn't overwhelm ("Tap to learn how")
314
+ 2. **Click → Visual guide** — for those who want details
315
+ 3. **Adaptive UI** — drawer on mobile, modal on desktop
316
+ 4. **Device-specific** — iOS Safari gets iOS-specific steps
317
+
318
+ ### Why Simplified?
319
+
320
+ The old version had:
321
+ - ❌ Engagement tracking (`actions`, `timeSpent`, `visitCount`)
322
+ - ❌ Complex metrics in localStorage
323
+ - ❌ Too many props (`delayMs`, `resetDays`, `engagementThreshold`)
324
+ - ❌ Multiple components for same job
325
+ - ❌ Over-engineered for Cmdop
326
+
327
+ The new version:
328
+ - ✅ One provider, two components, one hook
329
+ - ✅ Click-to-guide (minimal hint + visual guide on demand)
330
+ - ✅ Zero config
331
+ - ✅ No tracking
332
+ - ✅ Simple localStorage (only dismiss state)
333
+ - ✅ Works out of the box
334
+
335
+ ---
336
+
337
+ ## Troubleshooting
338
+
339
+ **iOS hint not showing?**
340
+ - Check if iOS Safari (not Chrome on iOS)
341
+ - Check if already installed
342
+ - Check if dismissed recently:
343
+ ```js
344
+ const dismissed = localStorage.getItem('cmdop-a2hs-dismissed');
345
+ if (dismissed) {
346
+ const days = (Date.now() - parseInt(dismissed)) / (1000 * 60 * 60 * 24);
347
+ console.log('Dismissed', days.toFixed(1), 'days ago');
348
+ }
349
+ ```
350
+ - Force reset: `localStorage.removeItem('cmdop-a2hs-dismissed')`
351
+ - **Hint reappears automatically after 3 days**
352
+
353
+ **Android button not showing?**
354
+ - Check PWA manifest (`/manifest.json`)
355
+ - Check service worker
356
+ - Check HTTPS (required for PWA)
357
+ - Open DevTools → Application → Manifest
358
+
359
+ **Already installed but still showing?**
360
+ - Clear localStorage: `localStorage.clear()`
361
+ - Reload page
362
+
363
+ ---
364
+
365
+ ## Browser Support
366
+
367
+ | Platform | Browser | Support |
368
+ |----------|---------|---------|
369
+ | iOS | Safari | ✅ Inline hint |
370
+ | iOS | Chrome/Firefox | ❌ No PWA support |
371
+ | Android | Chrome | ✅ Native prompt |
372
+ | Android | Firefox | ⚠️ Manual only |
373
+ | Desktop | Chrome/Edge | ✅ Native prompt |
374
+
375
+ ---
376
+
377
+ ## That's It
378
+
379
+ Three exports:
380
+ 1. `<InstallProvider>` — wrap your app
381
+ 2. `<A2HSHint />` — iOS hint
382
+ 3. `<InstallButton />` — Android button
383
+
384
+ One hook:
385
+ - `useInstall()` — access state
386
+
387
+ Zero config. Done.
@@ -0,0 +1,226 @@
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
+ * Optionally shows push notification prompt after PWA installation
14
+ * by providing pushNotifications.vapidPublicKey
15
+ */
16
+
17
+ import React, { useState, useEffect } from 'react';
18
+ import { Share, X, ChevronRight, Download } from 'lucide-react';
19
+ import { Button } from '@djangocfg/ui-nextjs';
20
+ import { cn } from '@djangocfg/ui-nextjs/lib';
21
+
22
+ import { useInstall } from '../context/InstallContext';
23
+ import { IOSGuide } from './IOSGuide';
24
+ import { PushPrompt } from './PushPrompt';
25
+ import { pwaLogger } from '../utils/logger';
26
+ import { markA2HSDismissed, isA2HSDismissedRecently } from '../utils/localStorage';
27
+ import type { PushNotificationOptions } from '../types';
28
+
29
+ const DEFAULT_RESET_DAYS = 3;
30
+
31
+ interface A2HSHintProps {
32
+ /**
33
+ * Additional class names for the container
34
+ */
35
+ className?: string;
36
+
37
+ /**
38
+ * Number of days before re-showing dismissed hint
39
+ * @default 3
40
+ * Set to null to never reset (show once forever)
41
+ */
42
+ resetAfterDays?: number | null;
43
+
44
+ /**
45
+ * Delay before showing hint (ms)
46
+ * @default 3000
47
+ */
48
+ delayMs?: number;
49
+
50
+ /**
51
+ * Force show on ANY browser (ignores platform detection)
52
+ * Useful for testing on desktop during development
53
+ * @default false
54
+ */
55
+ forceShow?: boolean;
56
+
57
+ /**
58
+ * App logo URL to display in hint
59
+ * If not provided, uses Share icon
60
+ */
61
+ logo?: string;
62
+
63
+ /**
64
+ * Enable push notifications prompt after PWA install
65
+ * Provide VAPID public key to enable
66
+ */
67
+ pushNotifications?: PushNotificationOptions & {
68
+ /**
69
+ * Delay before showing push prompt after PWA install (ms)
70
+ * @default 5000
71
+ */
72
+ delayMs?: number;
73
+ /**
74
+ * Number of days before re-showing dismissed push prompt
75
+ * @default 7
76
+ */
77
+ resetAfterDays?: number;
78
+ };
79
+ }
80
+
81
+ export function A2HSHint({
82
+ className,
83
+ resetAfterDays = DEFAULT_RESET_DAYS,
84
+ delayMs = 3000,
85
+ forceShow = false,
86
+ logo,
87
+ pushNotifications
88
+ }: A2HSHintProps = {}) {
89
+ const { isIOS, isSafari, isInstalled, canPrompt, install } = useInstall();
90
+ const [show, setShow] = useState(false);
91
+ const [showGuide, setShowGuide] = useState(false);
92
+ const [installing, setInstalling] = useState(false);
93
+
94
+ // Determine if should show hint
95
+ const shouldShow = forceShow || (!isInstalled && ((isIOS && isSafari) || canPrompt));
96
+
97
+ useEffect(() => {
98
+ // Only show on iOS Safari or Android Chrome (unless forceShow for dev testing)
99
+ if (!shouldShow) return;
100
+
101
+ // Check if previously dismissed (skip localStorage check if forceShow)
102
+ if (!forceShow && typeof window !== 'undefined') {
103
+ // If resetAfterDays is null, never reset (check with very large number)
104
+ if (resetAfterDays === null) {
105
+ if (isA2HSDismissedRecently(Number.MAX_SAFE_INTEGER)) {
106
+ return; // Dismissed forever
107
+ }
108
+ } else if (isA2HSDismissedRecently(resetAfterDays)) {
109
+ return; // Still within reset period
110
+ }
111
+ }
112
+
113
+ // Show after delay (user is already engaged)
114
+ const timer = setTimeout(() => setShow(true), delayMs);
115
+ return () => clearTimeout(timer);
116
+ }, [shouldShow, resetAfterDays, delayMs, forceShow]);
117
+
118
+ const handleDismiss = () => {
119
+ setShow(false);
120
+ // Don't save to localStorage if forceShow (dev testing mode)
121
+ if (!forceShow) {
122
+ markA2HSDismissed();
123
+ }
124
+ };
125
+
126
+ const handleGuideDismiss = () => {
127
+ setShowGuide(false);
128
+ // When guide is dismissed, also dismiss the hint
129
+ handleDismiss();
130
+ };
131
+
132
+ const handleClick = async () => {
133
+ // forceShow (dev mode) or iOS: Open visual guide
134
+ if (forceShow || (isIOS && isSafari)) {
135
+ setShowGuide(true);
136
+ } else if (canPrompt) {
137
+ // Android: Trigger native install prompt
138
+ setInstalling(true);
139
+ try {
140
+ await install();
141
+ // If install succeeds, dismiss hint
142
+ handleDismiss();
143
+ } catch (error) {
144
+ pwaLogger.error('[A2HSHint] Install error:', error);
145
+ } finally {
146
+ setInstalling(false);
147
+ }
148
+ }
149
+ };
150
+
151
+ if (!show) return null;
152
+
153
+ // Platform-specific content
154
+ const isIOSPlatform = isIOS && isSafari;
155
+ const title = isIOSPlatform ? 'Keep terminal with you' : 'Install Cmdop';
156
+ const subtitle = isIOSPlatform ? (
157
+ <>
158
+ Tap to learn how <ChevronRight className="w-3 h-3" />
159
+ </>
160
+ ) : (
161
+ <>
162
+ Tap to install <Download className="w-3 h-3" />
163
+ </>
164
+ );
165
+
166
+ return (
167
+ <>
168
+ <div className={cn(
169
+ "fixed bottom-4 left-4 right-4 z-50 animate-in slide-in-from-bottom-4 duration-300",
170
+ className
171
+ )}>
172
+ <div className="w-full bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-lg">
173
+ <div className="flex items-center gap-3">
174
+ {/* App logo or icon */}
175
+ <div className="flex-shrink-0">
176
+ {logo ? (
177
+ <img src={logo} alt="App logo" className="w-10 h-10 rounded-lg" />
178
+ ) : (
179
+ <Share className="w-5 h-5 text-blue-400" />
180
+ )}
181
+ </div>
182
+
183
+ {/* Content */}
184
+ <div className="flex-1 min-w-0">
185
+ <p className="text-sm font-medium text-white mb-1">{title}</p>
186
+ <Button
187
+ onClick={handleClick}
188
+ loading={installing}
189
+ size="sm"
190
+ variant="ghost"
191
+ className="text-xs text-zinc-400 hover:text-zinc-300 p-0 h-auto font-normal flex items-center gap-1"
192
+ >
193
+ {subtitle}
194
+ </Button>
195
+ </div>
196
+
197
+ {/* Close button */}
198
+ <Button
199
+ onClick={handleDismiss}
200
+ size="sm"
201
+ variant="ghost"
202
+ className="flex-shrink-0 p-1"
203
+ aria-label="Dismiss"
204
+ >
205
+ <X className="w-4 h-4" />
206
+ </Button>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ {/* iOS or forceShow: Detailed guide modal - dismissing guide also dismisses hint */}
212
+ {(isIOSPlatform || forceShow) && <IOSGuide open={showGuide} onDismiss={handleGuideDismiss} />}
213
+
214
+ {/* Push Notifications Prompt - shown after PWA install if enabled */}
215
+ {pushNotifications?.vapidPublicKey && (
216
+ <PushPrompt
217
+ vapidPublicKey={pushNotifications.vapidPublicKey}
218
+ subscribeEndpoint={pushNotifications.subscribeEndpoint}
219
+ requirePWA={true}
220
+ delayMs={pushNotifications.delayMs || 5000}
221
+ resetAfterDays={pushNotifications.resetAfterDays || 7}
222
+ />
223
+ )}
224
+ </>
225
+ );
226
+ }
@@ -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
+ }