@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.
- package/README.md +204 -18
- package/package.json +5 -5
- package/src/components/errors/index.ts +9 -0
- package/src/components/errors/types.ts +38 -0
- package/src/layouts/AppLayout/AppLayout.tsx +33 -45
- package/src/layouts/AppLayout/BaseApp.tsx +105 -28
- package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
- package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
- package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
- package/src/layouts/_components/UserMenu.tsx +1 -1
- package/src/layouts/index.ts +1 -1
- package/src/layouts/types/index.ts +47 -0
- package/src/layouts/types/layout.types.ts +61 -0
- package/src/layouts/types/providers.types.ts +65 -0
- package/src/layouts/types/ui.types.ts +103 -0
- package/src/snippets/Analytics/index.ts +1 -0
- package/src/snippets/Analytics/types.ts +10 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
- package/src/snippets/PWAInstall/@docs/README.md +92 -0
- package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
- package/src/snippets/PWAInstall/README.md +185 -0
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
- package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
- package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
- package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
- package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
- package/src/snippets/PWAInstall/index.ts +76 -0
- package/src/snippets/PWAInstall/types/components.ts +95 -0
- package/src/snippets/PWAInstall/types/config.ts +22 -0
- package/src/snippets/PWAInstall/types/index.ts +26 -0
- package/src/snippets/PWAInstall/types/install.ts +38 -0
- package/src/snippets/PWAInstall/types/platform.ts +29 -0
- package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
- package/src/snippets/PWAInstall/utils/logger.ts +149 -0
- package/src/snippets/PWAInstall/utils/platform.ts +151 -0
- package/src/snippets/PushNotifications/@docs/README.md +191 -0
- package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
- package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
- package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
- package/src/snippets/PushNotifications/README.md +328 -0
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +165 -0
- package/src/snippets/PushNotifications/config.ts +20 -0
- package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
- package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
- package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
- package/src/snippets/PushNotifications/index.ts +87 -0
- package/src/snippets/PushNotifications/types/config.ts +28 -0
- package/src/snippets/PushNotifications/types/index.ts +9 -0
- package/src/snippets/PushNotifications/types/push.ts +21 -0
- package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
- package/src/snippets/PushNotifications/utils/logger.ts +149 -0
- package/src/snippets/PushNotifications/utils/platform.ts +151 -0
- package/src/snippets/PushNotifications/utils/vapid.ts +226 -0
- package/src/snippets/index.ts +55 -0
- package/src/layouts/shared/index.ts +0 -21
- package/src/layouts/shared/types.ts +0 -211
- /package/src/layouts/{shared → types}/README.md +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized utilities for detecting PWA state, platform, and capabilities.
|
|
5
|
+
* Used by hooks to avoid code duplication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if running as PWA (standalone mode)
|
|
10
|
+
*
|
|
11
|
+
* Checks if the app is running in standalone mode (added to home screen).
|
|
12
|
+
* This is the primary indicator that a PWA has been installed.
|
|
13
|
+
*
|
|
14
|
+
* @returns true if app is running in standalone mode
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* if (isStandalone()) {
|
|
19
|
+
* console.log('Running as PWA');
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function isStandalone(): boolean {
|
|
24
|
+
if (typeof window === 'undefined') return false;
|
|
25
|
+
|
|
26
|
+
// Fallback for older browsers without matchMedia
|
|
27
|
+
if (!window.matchMedia) {
|
|
28
|
+
// Use legacy iOS check only
|
|
29
|
+
const nav = navigator as Navigator & { standalone?: boolean };
|
|
30
|
+
return nav.standalone === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check display-mode media query (modern approach)
|
|
34
|
+
const isStandaloneDisplay = window.matchMedia('(display-mode: standalone)').matches;
|
|
35
|
+
|
|
36
|
+
// Legacy iOS check (navigator.standalone)
|
|
37
|
+
const nav = navigator as Navigator & { standalone?: boolean };
|
|
38
|
+
const isStandaloneNavigator = nav.standalone === true;
|
|
39
|
+
|
|
40
|
+
return isStandaloneDisplay || isStandaloneNavigator;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if device is mobile
|
|
45
|
+
*
|
|
46
|
+
* @returns true if device is mobile (iOS, Android, etc.)
|
|
47
|
+
*/
|
|
48
|
+
export function isMobileDevice(): boolean {
|
|
49
|
+
if (typeof window === 'undefined') return false;
|
|
50
|
+
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if web app manifest exists and is valid
|
|
55
|
+
*
|
|
56
|
+
* @returns true if manifest link exists in document head
|
|
57
|
+
*/
|
|
58
|
+
export function hasValidManifest(): boolean {
|
|
59
|
+
if (typeof document === 'undefined') return false;
|
|
60
|
+
const manifestLink = document.querySelector('link[rel="manifest"]');
|
|
61
|
+
return !!manifestLink;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reliable check for PWA mode with edge case handling
|
|
66
|
+
*
|
|
67
|
+
* This function provides additional validation for desktop browsers
|
|
68
|
+
* to avoid false positives (e.g., Safari macOS "Add to Dock").
|
|
69
|
+
*
|
|
70
|
+
* For mobile devices, standard standalone check is sufficient.
|
|
71
|
+
* For desktop, additionally validates that a manifest exists.
|
|
72
|
+
*
|
|
73
|
+
* @returns true if app is running as a genuine PWA
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* // Use this for more reliable detection
|
|
78
|
+
* if (isStandaloneReliable()) {
|
|
79
|
+
* console.log('Definitely running as PWA');
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function isStandaloneReliable(): boolean {
|
|
84
|
+
const standalone = isStandalone();
|
|
85
|
+
if (!standalone) return false;
|
|
86
|
+
|
|
87
|
+
// For mobile devices, standalone check is sufficient
|
|
88
|
+
if (isMobileDevice()) return true;
|
|
89
|
+
|
|
90
|
+
// For desktop browsers, additionally check for valid manifest
|
|
91
|
+
// This prevents false positives like Safari macOS "Add to Dock"
|
|
92
|
+
return hasValidManifest();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get display mode from media query
|
|
97
|
+
*
|
|
98
|
+
* @returns Current display mode: 'standalone', 'fullscreen', 'minimal-ui', or 'browser'
|
|
99
|
+
*/
|
|
100
|
+
export function getDisplayMode(): 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser' {
|
|
101
|
+
if (typeof window === 'undefined') return 'browser';
|
|
102
|
+
|
|
103
|
+
if (!window.matchMedia) return 'browser';
|
|
104
|
+
|
|
105
|
+
const modes: Array<'standalone' | 'fullscreen' | 'minimal-ui'> = [
|
|
106
|
+
'fullscreen',
|
|
107
|
+
'standalone',
|
|
108
|
+
'minimal-ui',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const mode of modes) {
|
|
112
|
+
if (window.matchMedia(`(display-mode: ${mode})`).matches) {
|
|
113
|
+
return mode;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 'browser';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a media query listener for display-mode changes
|
|
122
|
+
*
|
|
123
|
+
* @param callback - Function to call when display mode changes
|
|
124
|
+
* @returns Cleanup function to remove listener
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const cleanup = onDisplayModeChange((isStandalone) => {
|
|
129
|
+
* console.log('Display mode changed:', isStandalone);
|
|
130
|
+
* });
|
|
131
|
+
*
|
|
132
|
+
* // Later: cleanup();
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function onDisplayModeChange(callback: (isStandalone: boolean) => void): () => void {
|
|
136
|
+
if (typeof window === 'undefined' || !window.matchMedia) {
|
|
137
|
+
return () => {}; // No-op cleanup
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const mediaQuery = window.matchMedia('(display-mode: standalone)');
|
|
141
|
+
|
|
142
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
143
|
+
callback(e.matches);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VAPID Key Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides validation and conversion utilities for VAPID public keys
|
|
5
|
+
* used in push notification subscriptions.
|
|
6
|
+
*
|
|
7
|
+
* VAPID keys must be:
|
|
8
|
+
* - Base64url encoded
|
|
9
|
+
* - 65 bytes after decoding (P-256 uncompressed public key)
|
|
10
|
+
* - Start with 0x04 (uncompressed point indicator)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class for VAPID key validation failures
|
|
15
|
+
*/
|
|
16
|
+
export class VapidKeyError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
public readonly code: VapidKeyErrorCode
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'VapidKeyError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error codes for VAPID key validation
|
|
28
|
+
*/
|
|
29
|
+
export type VapidKeyErrorCode =
|
|
30
|
+
| 'VAPID_EMPTY'
|
|
31
|
+
| 'VAPID_INVALID_TYPE'
|
|
32
|
+
| 'VAPID_INVALID_BASE64'
|
|
33
|
+
| 'VAPID_INVALID_LENGTH'
|
|
34
|
+
| 'VAPID_INVALID_FORMAT';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert base64url VAPID public key to Uint8Array
|
|
38
|
+
*
|
|
39
|
+
* Validates and converts a VAPID public key from base64url format
|
|
40
|
+
* to Uint8Array for use with PushManager.subscribe().
|
|
41
|
+
*
|
|
42
|
+
* @param base64String - VAPID public key in base64url format
|
|
43
|
+
* @returns Uint8Array ready for pushManager.subscribe()
|
|
44
|
+
* @throws VapidKeyError if key is invalid
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* try {
|
|
49
|
+
* const key = urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);
|
|
50
|
+
* await registration.pushManager.subscribe({
|
|
51
|
+
* userVisibleOnly: true,
|
|
52
|
+
* applicationServerKey: key,
|
|
53
|
+
* });
|
|
54
|
+
* } catch (e) {
|
|
55
|
+
* if (e instanceof VapidKeyError) {
|
|
56
|
+
* console.error('Invalid VAPID key:', e.message, e.code);
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
62
|
+
// 1. Validate input
|
|
63
|
+
if (!base64String) {
|
|
64
|
+
throw new VapidKeyError('VAPID public key is required', 'VAPID_EMPTY');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof base64String !== 'string') {
|
|
68
|
+
throw new VapidKeyError('VAPID public key must be a string', 'VAPID_INVALID_TYPE');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Convert base64url to base64
|
|
72
|
+
// base64url uses '-' and '_' instead of '+' and '/'
|
|
73
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
74
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
75
|
+
|
|
76
|
+
// 3. Decode base64
|
|
77
|
+
let rawData: string;
|
|
78
|
+
try {
|
|
79
|
+
rawData = window.atob(base64);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
throw new VapidKeyError(
|
|
82
|
+
`Invalid base64url format: ${e instanceof Error ? e.message : String(e)}`,
|
|
83
|
+
'VAPID_INVALID_BASE64'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Convert to Uint8Array
|
|
88
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
89
|
+
for (let i = 0; i < rawData.length; i++) {
|
|
90
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Validate length (P-256 uncompressed = 65 bytes)
|
|
94
|
+
// Format: 0x04 (1 byte) + X coordinate (32 bytes) + Y coordinate (32 bytes)
|
|
95
|
+
if (outputArray.length !== 65) {
|
|
96
|
+
throw new VapidKeyError(
|
|
97
|
+
`Invalid key length: expected 65 bytes (P-256 uncompressed), got ${outputArray.length} bytes`,
|
|
98
|
+
'VAPID_INVALID_LENGTH'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 6. Validate format (must start with 0x04 for uncompressed P-256 point)
|
|
103
|
+
if (outputArray[0] !== 0x04) {
|
|
104
|
+
throw new VapidKeyError(
|
|
105
|
+
`Invalid key format: must start with 0x04 (uncompressed P-256 point), got 0x${outputArray[0]
|
|
106
|
+
.toString(16)
|
|
107
|
+
.padStart(2, '0')}`,
|
|
108
|
+
'VAPID_INVALID_FORMAT'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return outputArray;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate VAPID key without conversion
|
|
117
|
+
*
|
|
118
|
+
* Checks if a VAPID key is valid without converting it.
|
|
119
|
+
* Useful for validation before attempting subscription.
|
|
120
|
+
*
|
|
121
|
+
* @param base64String - VAPID public key to validate
|
|
122
|
+
* @returns true if key is valid
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* if (isValidVapidKey(vapidKey)) {
|
|
127
|
+
* // Proceed with subscription
|
|
128
|
+
* } else {
|
|
129
|
+
* console.error('Invalid VAPID key configuration');
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function isValidVapidKey(base64String: string): boolean {
|
|
134
|
+
try {
|
|
135
|
+
urlBase64ToUint8Array(base64String);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get VAPID key information for debugging
|
|
144
|
+
*
|
|
145
|
+
* Returns detailed information about a VAPID key for troubleshooting.
|
|
146
|
+
*
|
|
147
|
+
* @param base64String - VAPID public key to inspect
|
|
148
|
+
* @returns Key information object
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const info = getVapidKeyInfo(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);
|
|
153
|
+
* console.log('VAPID Key Info:', info);
|
|
154
|
+
* // {
|
|
155
|
+
* // valid: true,
|
|
156
|
+
* // length: 65,
|
|
157
|
+
* // firstByte: '0x04',
|
|
158
|
+
* // format: 'P-256 uncompressed'
|
|
159
|
+
* // }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function getVapidKeyInfo(base64String: string): {
|
|
163
|
+
valid: boolean;
|
|
164
|
+
length?: number;
|
|
165
|
+
firstByte?: string;
|
|
166
|
+
format?: string;
|
|
167
|
+
error?: string;
|
|
168
|
+
errorCode?: VapidKeyErrorCode;
|
|
169
|
+
} {
|
|
170
|
+
try {
|
|
171
|
+
const key = urlBase64ToUint8Array(base64String);
|
|
172
|
+
return {
|
|
173
|
+
valid: true,
|
|
174
|
+
length: key.length,
|
|
175
|
+
firstByte: `0x${key[0].toString(16).padStart(2, '0')}`,
|
|
176
|
+
format: key[0] === 0x04 ? 'P-256 uncompressed' : 'Unknown',
|
|
177
|
+
};
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (e instanceof VapidKeyError) {
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
error: e.message,
|
|
183
|
+
errorCode: e.code,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
valid: false,
|
|
188
|
+
error: String(e),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Safe VAPID key conversion with error handling
|
|
195
|
+
*
|
|
196
|
+
* Attempts to convert a VAPID key with detailed error logging.
|
|
197
|
+
* Returns null on failure instead of throwing.
|
|
198
|
+
*
|
|
199
|
+
* @param base64String - VAPID public key
|
|
200
|
+
* @param onError - Optional error callback
|
|
201
|
+
* @returns Uint8Array or null if conversion fails
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const key = safeUrlBase64ToUint8Array(vapidKey, (error) => {
|
|
206
|
+
* console.error('VAPID conversion failed:', error.message, error.code);
|
|
207
|
+
* });
|
|
208
|
+
*
|
|
209
|
+
* if (key) {
|
|
210
|
+
* // Use key for subscription
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function safeUrlBase64ToUint8Array(
|
|
215
|
+
base64String: string,
|
|
216
|
+
onError?: (error: VapidKeyError) => void
|
|
217
|
+
): Uint8Array | null {
|
|
218
|
+
try {
|
|
219
|
+
return urlBase64ToUint8Array(base64String);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
if (e instanceof VapidKeyError) {
|
|
222
|
+
onError?.(e);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/snippets/index.ts
CHANGED
|
@@ -43,3 +43,58 @@ export type {
|
|
|
43
43
|
UseMcpChatReturn,
|
|
44
44
|
AskAIButtonProps,
|
|
45
45
|
} from './McpChat';
|
|
46
|
+
|
|
47
|
+
// PWA Install (Progressive Web App Installation)
|
|
48
|
+
export {
|
|
49
|
+
PwaProvider,
|
|
50
|
+
useInstall,
|
|
51
|
+
A2HSHint,
|
|
52
|
+
IOSGuide,
|
|
53
|
+
DesktopGuide,
|
|
54
|
+
useIsPWA,
|
|
55
|
+
clearIsPWACache,
|
|
56
|
+
clearAllPWAInstallData,
|
|
57
|
+
isStandalone,
|
|
58
|
+
isStandaloneReliable,
|
|
59
|
+
isMobileDevice,
|
|
60
|
+
hasValidManifest,
|
|
61
|
+
getDisplayMode,
|
|
62
|
+
onDisplayModeChange,
|
|
63
|
+
} from './PWAInstall';
|
|
64
|
+
export type {
|
|
65
|
+
PlatformInfo,
|
|
66
|
+
InstallPromptState,
|
|
67
|
+
BeforeInstallPromptEvent,
|
|
68
|
+
InstallOutcome,
|
|
69
|
+
IOSGuideState,
|
|
70
|
+
InstallContextType,
|
|
71
|
+
InstallManagerProps,
|
|
72
|
+
AndroidInstallButtonProps,
|
|
73
|
+
IOSGuideModalProps,
|
|
74
|
+
InstallStep,
|
|
75
|
+
UseIsPWAOptions,
|
|
76
|
+
} from './PWAInstall';
|
|
77
|
+
|
|
78
|
+
// Push Notifications
|
|
79
|
+
export {
|
|
80
|
+
PushProvider,
|
|
81
|
+
usePush,
|
|
82
|
+
DjangoPushProvider,
|
|
83
|
+
useDjangoPushContext,
|
|
84
|
+
PushPrompt,
|
|
85
|
+
usePushNotifications,
|
|
86
|
+
useDjangoPush,
|
|
87
|
+
DEFAULT_VAPID_PUBLIC_KEY,
|
|
88
|
+
urlBase64ToUint8Array,
|
|
89
|
+
isValidVapidKey,
|
|
90
|
+
getVapidKeyInfo,
|
|
91
|
+
safeUrlBase64ToUint8Array,
|
|
92
|
+
VapidKeyError,
|
|
93
|
+
clearAllPushData,
|
|
94
|
+
} from './PushNotifications';
|
|
95
|
+
export type {
|
|
96
|
+
PushNotificationState,
|
|
97
|
+
PushNotificationOptions,
|
|
98
|
+
PushMessage,
|
|
99
|
+
VapidKeyErrorCode,
|
|
100
|
+
} from './PushNotifications';
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Layouts Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type {
|
|
6
|
-
ThemeConfig,
|
|
7
|
-
LayoutErrorTrackingConfig,
|
|
8
|
-
ErrorBoundaryConfig,
|
|
9
|
-
SWRConfigOptions,
|
|
10
|
-
McpChatConfig,
|
|
11
|
-
BaseLayoutProps,
|
|
12
|
-
NavigationItem,
|
|
13
|
-
NavigationSection,
|
|
14
|
-
FooterLink,
|
|
15
|
-
FooterMenuSection,
|
|
16
|
-
FooterSocialLinks,
|
|
17
|
-
FooterConfig,
|
|
18
|
-
UserMenuItem,
|
|
19
|
-
UserMenuGroup,
|
|
20
|
-
UserMenuConfig,
|
|
21
|
-
} from './types';
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Types for All Layouts
|
|
3
|
-
*
|
|
4
|
-
* Universal type system to avoid duplication across layouts
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ReactNode } from 'react';
|
|
8
|
-
import type { LucideIcon } from 'lucide-react';
|
|
9
|
-
import type { AuthConfig } from '@djangocfg/api/auth';
|
|
10
|
-
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Theme Configuration
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
export interface ThemeConfig {
|
|
16
|
-
defaultTheme?: 'light' | 'dark' | 'system';
|
|
17
|
-
storageKey?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Error Tracking Configuration
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Error tracking configuration for layouts
|
|
26
|
-
* Note: Import detailed types from @djangocfg/layouts/components if needed
|
|
27
|
-
*/
|
|
28
|
-
export interface LayoutErrorTrackingConfig {
|
|
29
|
-
validation?: {
|
|
30
|
-
enabled?: boolean;
|
|
31
|
-
showToast?: boolean;
|
|
32
|
-
};
|
|
33
|
-
cors?: {
|
|
34
|
-
enabled?: boolean;
|
|
35
|
-
showToast?: boolean;
|
|
36
|
-
};
|
|
37
|
-
network?: {
|
|
38
|
-
enabled?: boolean;
|
|
39
|
-
showToast?: boolean;
|
|
40
|
-
};
|
|
41
|
-
onError?: (error: any) => boolean | void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// Error Boundary Configuration
|
|
46
|
-
// ============================================================================
|
|
47
|
-
|
|
48
|
-
export interface ErrorBoundaryConfig {
|
|
49
|
-
enabled?: boolean;
|
|
50
|
-
supportEmail?: string;
|
|
51
|
-
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// SWR Configuration
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
|
-
export interface SWRConfigOptions {
|
|
59
|
-
revalidateOnFocus?: boolean;
|
|
60
|
-
revalidateOnReconnect?: boolean;
|
|
61
|
-
dedupingInterval?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ============================================================================
|
|
65
|
-
// MCP Chat Configuration
|
|
66
|
-
// ============================================================================
|
|
67
|
-
|
|
68
|
-
export interface McpChatConfig {
|
|
69
|
-
/** Enable MCP chat widget */
|
|
70
|
-
enabled?: boolean;
|
|
71
|
-
/** API endpoint for chat */
|
|
72
|
-
apiEndpoint?: string;
|
|
73
|
-
/** Chat widget title */
|
|
74
|
-
title?: string;
|
|
75
|
-
/** Input placeholder */
|
|
76
|
-
placeholder?: string;
|
|
77
|
-
/** Greeting message */
|
|
78
|
-
greeting?: string;
|
|
79
|
-
/** Widget position */
|
|
80
|
-
position?: 'bottom-right' | 'bottom-left';
|
|
81
|
-
/** Widget variant */
|
|
82
|
-
variant?: 'default' | 'minimal';
|
|
83
|
-
/** Enable streaming responses */
|
|
84
|
-
enableStreaming?: boolean;
|
|
85
|
-
/** Auto-detect environment (dev/prod) */
|
|
86
|
-
autoDetectEnvironment?: boolean;
|
|
87
|
-
/** Custom class name */
|
|
88
|
-
className?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ============================================================================
|
|
92
|
-
// Base Layout Props
|
|
93
|
-
// ============================================================================
|
|
94
|
-
|
|
95
|
-
export interface BaseLayoutProps {
|
|
96
|
-
children: ReactNode;
|
|
97
|
-
|
|
98
|
-
/** Theme configuration */
|
|
99
|
-
theme?: ThemeConfig;
|
|
100
|
-
|
|
101
|
-
/** Auth configuration */
|
|
102
|
-
auth?: AuthConfig;
|
|
103
|
-
|
|
104
|
-
/** Error tracking configuration */
|
|
105
|
-
errorTracking?: LayoutErrorTrackingConfig;
|
|
106
|
-
|
|
107
|
-
/** SWR configuration */
|
|
108
|
-
swr?: SWRConfigOptions;
|
|
109
|
-
|
|
110
|
-
/** Error boundary configuration (enabled by default) */
|
|
111
|
-
errorBoundary?: ErrorBoundaryConfig;
|
|
112
|
-
|
|
113
|
-
/** MCP chat configuration */
|
|
114
|
-
mcpChat?: McpChatConfig;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ============================================================================
|
|
118
|
-
// Navigation Types
|
|
119
|
-
// ============================================================================
|
|
120
|
-
|
|
121
|
-
export interface NavigationItem {
|
|
122
|
-
label: string;
|
|
123
|
-
href: string;
|
|
124
|
-
icon?: LucideIcon | string;
|
|
125
|
-
badge?: string | number;
|
|
126
|
-
external?: boolean;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface NavigationSection {
|
|
130
|
-
title?: string;
|
|
131
|
-
items: NavigationItem[];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ============================================================================
|
|
135
|
-
// Footer Types
|
|
136
|
-
// ============================================================================
|
|
137
|
-
|
|
138
|
-
export interface FooterLink {
|
|
139
|
-
label: string;
|
|
140
|
-
path: string;
|
|
141
|
-
external?: boolean;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export interface FooterMenuSection {
|
|
145
|
-
title: string;
|
|
146
|
-
items: FooterLink[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export interface FooterSocialLinks {
|
|
150
|
-
github?: string;
|
|
151
|
-
linkedin?: string;
|
|
152
|
-
twitter?: string;
|
|
153
|
-
telegram?: string;
|
|
154
|
-
youtube?: string;
|
|
155
|
-
facebook?: string;
|
|
156
|
-
instagram?: string;
|
|
157
|
-
whatsapp?: string;
|
|
158
|
-
email?: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface FooterConfig {
|
|
162
|
-
siteName?: string;
|
|
163
|
-
description?: string;
|
|
164
|
-
logo?: string;
|
|
165
|
-
badge?: {
|
|
166
|
-
icon: LucideIcon;
|
|
167
|
-
text: string;
|
|
168
|
-
};
|
|
169
|
-
socialLinks?: FooterSocialLinks;
|
|
170
|
-
links?: FooterLink[];
|
|
171
|
-
menuSections?: FooterMenuSection[];
|
|
172
|
-
copyright?: string;
|
|
173
|
-
credits?: {
|
|
174
|
-
text: string;
|
|
175
|
-
url?: string;
|
|
176
|
-
};
|
|
177
|
-
variant?: 'full' | 'simple';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ============================================================================
|
|
181
|
-
// User Menu Types
|
|
182
|
-
// ============================================================================
|
|
183
|
-
|
|
184
|
-
export interface UserMenuItem {
|
|
185
|
-
/** Menu item label */
|
|
186
|
-
label: string;
|
|
187
|
-
/** Link href (optional if onClick is provided) */
|
|
188
|
-
href?: string;
|
|
189
|
-
/** Icon component */
|
|
190
|
-
icon?: LucideIcon;
|
|
191
|
-
/** Visual variant */
|
|
192
|
-
variant?: 'default' | 'destructive';
|
|
193
|
-
/** Click handler (optional if href is provided) */
|
|
194
|
-
onClick?: () => void;
|
|
195
|
-
/** Open link in new tab */
|
|
196
|
-
external?: boolean;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface UserMenuGroup {
|
|
200
|
-
/** Optional group title (renders DropdownMenuLabel) */
|
|
201
|
-
title?: string;
|
|
202
|
-
/** Menu items in this group */
|
|
203
|
-
items: UserMenuItem[];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface UserMenuConfig {
|
|
207
|
-
/** Menu groups for authenticated users */
|
|
208
|
-
groups?: UserMenuGroup[];
|
|
209
|
-
/** Auth page path (for sign in button) */
|
|
210
|
-
authPath?: string;
|
|
211
|
-
}
|
|
File without changes
|