@djangocfg/ui-nextjs 2.1.147 → 2.1.149

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 CHANGED
@@ -16,9 +16,9 @@ pnpm add @djangocfg/ui-nextjs
16
16
 
17
17
  ```
18
18
  @djangocfg/ui-nextjs
19
- ├── Re-exports everything from @djangocfg/ui-core (60 components, 10 hooks)
19
+ ├── Re-exports everything from @djangocfg/ui-core (60 components, 13 hooks)
20
20
  ├── + Next.js specific components (11)
21
- ├── + Browser storage hooks (5)
21
+ ├── + Browser storage hooks (2)
22
22
  ├── + Blocks, Theme, Animations
23
23
  ```
24
24
 
@@ -60,26 +60,51 @@ import { Hero } from '@djangocfg/ui-nextjs/blocks';
60
60
 
61
61
  ## Hooks
62
62
 
63
- ### From ui-core (10)
64
- `useMediaQuery` `useIsMobile` `useCopy` `useCountdown` `useDebounce` `useDebouncedCallback` `useImageLoader` `useToast` `useEventListener` `useDebugTools`
63
+ ### From ui-core (13)
65
64
 
66
- ### Next.js/Browser Specific (5)
65
+ | Hook | Description |
66
+ |------|-------------|
67
+ | `useMediaQuery` | Responsive breakpoints |
68
+ | `useIsMobile` | Mobile detection |
69
+ | `useCopy` | Copy to clipboard |
70
+ | `useCountdown` | Countdown timer |
71
+ | `useDebounce` | Debounce values |
72
+ | `useDebouncedCallback` | Debounced callbacks |
73
+ | `useImageLoader` | Image loading state |
74
+ | `useToast` / `toast` | Toast notifications (Sonner) |
75
+ | `useEventListener` | Event bus |
76
+ | `useDebugTools` | Debug utilities |
77
+ | `useHotkey` | Keyboard shortcuts |
78
+ | `useBrowserDetect` | Browser detection |
79
+ | `useDeviceDetect` | Device detection |
80
+
81
+ ### Next.js Specific (2)
67
82
 
68
83
  | Hook | Description |
69
84
  |------|-------------|
85
+ | `useResolvedTheme` | Light/dark theme resolution (next-themes) |
70
86
  | `useLocalStorage` | Persistent state in localStorage |
71
- | `useSessionStorage` | Session-scoped state |
72
- | `useTheme` | Light/dark theme (next-themes) |
73
- | `useQueryParams` | URL query params (next/router) |
74
- | `useCfgRouter` | Enhanced Next.js router |
87
+
88
+ Note: `useSessionStorage` is also available in ui-core.
89
+
90
+ ## Dialog Service
91
+
92
+ The dialog service from ui-core is available for replacing native dialogs:
93
+
94
+ ```tsx
95
+ import { DialogProvider, useDialog, dialog } from '@djangocfg/ui-core/lib/dialog-service';
96
+ ```
97
+
98
+ See ui-core README for full documentation.
75
99
 
76
100
  ## Usage
77
101
 
78
102
  ```tsx
79
103
  import {
80
104
  Button, Card, Input, // from ui-core
81
- NextButtonLink, Sidebar, useLocalStorage // Next.js specific
105
+ NextButtonLink, Sidebar // Next.js specific
82
106
  } from '@djangocfg/ui-nextjs';
107
+ import { useLocalStorage } from '@djangocfg/ui-nextjs/hooks';
83
108
 
84
109
  function Example() {
85
110
  const [saved, setSaved] = useLocalStorage('form', null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.147",
3
+ "version": "2.1.149",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -80,9 +80,9 @@
80
80
  "check": "tsc --noEmit"
81
81
  },
82
82
  "peerDependencies": {
83
- "@djangocfg/api": "^2.1.147",
84
- "@djangocfg/ui-core": "^2.1.147",
85
- "@djangocfg/ui-tools": "^2.1.147",
83
+ "@djangocfg/api": "^2.1.149",
84
+ "@djangocfg/ui-core": "^2.1.149",
85
+ "@djangocfg/ui-tools": "^2.1.149",
86
86
  "@types/react": "^19.1.0",
87
87
  "@types/react-dom": "^19.1.0",
88
88
  "consola": "^3.4.2",
@@ -101,12 +101,10 @@
101
101
  "cytoscape": "^3.33.1",
102
102
  "cytoscape-cose-bilkent": "^4.1.0",
103
103
  "next-themes": "^0.4.6",
104
- "react-chartjs-2": "^5.3.0",
105
- "react-device-detect": "^2.2.3",
106
- "react-hotkeys-hook": "^5.2.1"
104
+ "react-chartjs-2": "^5.3.0"
107
105
  },
108
106
  "devDependencies": {
109
- "@djangocfg/typescript-config": "^2.1.147",
107
+ "@djangocfg/typescript-config": "^2.1.149",
110
108
  "@radix-ui/react-dropdown-menu": "^2.1.16",
111
109
  "@radix-ui/react-slot": "^1.2.4",
112
110
  "@types/node": "^24.7.2",
@@ -9,14 +9,23 @@
9
9
  export { useResolvedTheme } from './useResolvedTheme';
10
10
  export type { ResolvedTheme } from './useResolvedTheme';
11
11
 
12
- // Keyboard shortcuts
13
- export { useHotkey, useHotkeysContext, HotkeysProvider, isHotkeyPressed } from './useHotkey';
14
- export type { UseHotkeyOptions, HotkeyCallback, Keys } from './useHotkey';
12
+ // Re-export from ui-core
13
+ export {
14
+ // Keyboard shortcuts
15
+ useHotkey,
16
+ useHotkeysContext,
17
+ HotkeysProvider,
18
+ isHotkeyPressed,
19
+ // Device detection
20
+ useDeviceDetect,
21
+ // Browser detection
22
+ useBrowserDetect,
23
+ } from '@djangocfg/ui-core/hooks';
15
24
 
16
- // Device detection
17
- export { useDeviceDetect } from './useDeviceDetect';
18
- export type { DeviceDetectResult } from './useDeviceDetect';
19
-
20
- // Browser detection (advanced - detects Chromium browsers correctly)
21
- export { useBrowserDetect } from './useBrowserDetect';
22
- export type { BrowserInfo } from './useBrowserDetect';
25
+ export type {
26
+ UseHotkeyOptions,
27
+ HotkeyCallback,
28
+ Keys,
29
+ DeviceDetectResult,
30
+ BrowserInfo,
31
+ } from '@djangocfg/ui-core/hooks';
@@ -1,384 +0,0 @@
1
- /**
2
- * Advanced browser detection hook
3
- *
4
- * Detects modern browsers including Chromium-based browsers that may
5
- * incorrectly report as Safari (Arc, Brave, Vivaldi, Comet, etc.)
6
- */
7
-
8
- 'use client';
9
-
10
- import { useMemo } from 'react';
11
-
12
- export interface BrowserInfo {
13
- // Core browser types
14
- isChrome: boolean;
15
- isChromium: boolean; // Any Chromium-based browser
16
- isSafari: boolean; // Real Safari (WebKit on macOS/iOS)
17
- isFirefox: boolean;
18
- isEdge: boolean;
19
- isOpera: boolean;
20
-
21
- // Modern Chromium-based browsers
22
- isBrave: boolean;
23
- isArc: boolean;
24
- isVivaldi: boolean;
25
- isYandex: boolean;
26
- isSamsungBrowser: boolean;
27
- isUCBrowser: boolean;
28
-
29
- // Additional browsers
30
- isComet: boolean; // Perplexity's Comet browser (Chromium-based, supports push)
31
- isOperaMini: boolean; // Opera Mini (does NOT support push notifications)
32
- isIE: boolean; // Internet Explorer (does NOT support push notifications)
33
-
34
- // In-App Browsers (WebViews) - typically do NOT support push notifications
35
- isFacebookInApp: boolean; // Facebook's in-app browser
36
- isInstagramInApp: boolean; // Instagram's in-app browser
37
- isTikTokInApp: boolean; // TikTok's in-app browser
38
- isSnapchatInApp: boolean; // Snapchat's in-app browser
39
- isWeChatInApp: boolean; // WeChat's in-app browser
40
- isThreadsInApp: boolean; // Threads' in-app browser
41
- isLinkedInInApp: boolean; // LinkedIn's in-app browser (uses Chrome WebView - supports push on Android)
42
- isTwitterInApp: boolean; // Twitter/X's in-app browser (uses Chrome WebView - supports push on Android)
43
- isInAppBrowser: boolean; // Any in-app browser detected
44
- isWebView: boolean; // Generic WebView detection
45
-
46
- // Browser name
47
- browserName: string;
48
-
49
- // Engine
50
- isWebKit: boolean; // Safari's engine
51
- isBlink: boolean; // Chromium's engine
52
- isGecko: boolean; // Firefox's engine
53
-
54
- // Push notification support
55
- /**
56
- * Whether the browser supports Web Push Notifications.
57
- * Returns false for browsers known to NOT support push:
58
- * - Opera Mini (no service worker support)
59
- * - Internet Explorer (deprecated, no Push API)
60
- * - UC Browser (unreliable push support)
61
- * - In-App browsers (Facebook, Instagram, TikTok, Snapchat, etc.)
62
- * - Generic WebViews (except Twitter/LinkedIn on Android which use Chrome WebView)
63
- *
64
- * Note: Comet (Perplexity) is Chromium-based and DOES support push notifications.
65
- * Note: This is a browser-level check. For full push support,
66
- * also check 'serviceWorker' in navigator && 'PushManager' in window
67
- */
68
- supportsPushNotifications: boolean;
69
-
70
- // iOS specific
71
- isIOSBrowser: boolean; // Any browser on iOS (all use WebKit, limited push support)
72
-
73
- // For debugging
74
- userAgent: string;
75
- }
76
-
77
- /**
78
- * Detect browser with improved accuracy for Chromium-based browsers
79
- *
80
- * @example
81
- * ```tsx
82
- * const browser = useBrowserDetect();
83
- *
84
- * if (browser.isSafari && !browser.isChromium) {
85
- * // Real Safari
86
- * }
87
- *
88
- * if (browser.isChromium) {
89
- * // Any Chromium-based browser (Chrome, Edge, Brave, Arc, etc.)
90
- * }
91
- * ```
92
- */
93
- export function useBrowserDetect(): BrowserInfo {
94
- return useMemo(() => {
95
- if (typeof window === 'undefined') {
96
- return {
97
- isChrome: false,
98
- isChromium: false,
99
- isSafari: false,
100
- isFirefox: false,
101
- isEdge: false,
102
- isOpera: false,
103
- isBrave: false,
104
- isArc: false,
105
- isVivaldi: false,
106
- isYandex: false,
107
- isSamsungBrowser: false,
108
- isUCBrowser: false,
109
- isComet: false,
110
- isOperaMini: false,
111
- isIE: false,
112
- isFacebookInApp: false,
113
- isInstagramInApp: false,
114
- isTikTokInApp: false,
115
- isSnapchatInApp: false,
116
- isWeChatInApp: false,
117
- isThreadsInApp: false,
118
- isLinkedInInApp: false,
119
- isTwitterInApp: false,
120
- isInAppBrowser: false,
121
- isWebView: false,
122
- browserName: 'unknown',
123
- isWebKit: false,
124
- isBlink: false,
125
- isGecko: false,
126
- supportsPushNotifications: false,
127
- isIOSBrowser: false,
128
- userAgent: '',
129
- };
130
- }
131
-
132
- const ua = window.navigator.userAgent.toLowerCase();
133
-
134
- // Check for specific browsers first (most specific to least specific)
135
-
136
- // Edge (Chromium-based)
137
- const isEdge = ua.includes('edg/') || ua.includes('edge/');
138
-
139
- // Brave (check for Brave-specific API)
140
- const isBrave = !!(window.navigator as any).brave;
141
-
142
- // Arc (check for Arc-specific markers in UA)
143
- const isArc = ua.includes('arc/');
144
-
145
- // Vivaldi
146
- const isVivaldi = ua.includes('vivaldi');
147
-
148
- // Yandex Browser
149
- const isYandex = ua.includes('yabrowser');
150
-
151
- // Samsung Internet
152
- const isSamsungBrowser = ua.includes('samsungbrowser');
153
-
154
- // UC Browser
155
- const isUCBrowser = ua.includes('ucbrowser') || ua.includes('uc browser');
156
-
157
- // Comet Browser (Perplexity's AI browser, Chromium-based)
158
- // May have 'comet' in UA or can be detected by specific markers
159
- const isComet = ua.includes('comet') || ua.includes('perplexity');
160
-
161
- // Opera Mini (does NOT support service workers or push)
162
- const isOperaMini = ua.includes('opera mini') || ua.includes('opios');
163
-
164
- // Internet Explorer (deprecated, no Push API support)
165
- const isIE = ua.includes('msie') || ua.includes('trident/');
166
-
167
- // ============================================================================
168
- // In-App Browsers Detection (WebViews)
169
- // These browsers typically do NOT support web push notifications
170
- // ============================================================================
171
-
172
- // Facebook In-App Browser
173
- // UA contains: FBAN (Facebook App Name) or FBAV (Facebook App Version)
174
- const isFacebookInApp = ua.includes('fban') || ua.includes('fbav') || ua.includes('fb_iab');
175
-
176
- // Instagram In-App Browser
177
- // UA contains: Instagram
178
- const isInstagramInApp = ua.includes('instagram');
179
-
180
- // TikTok In-App Browser
181
- // UA contains: TikTok or BytedanceWebview or ByteLocale
182
- const isTikTokInApp = ua.includes('tiktok') || ua.includes('bytedancewebview') || ua.includes('bytelocale');
183
-
184
- // Snapchat In-App Browser
185
- // UA contains: Snapchat
186
- const isSnapchatInApp = ua.includes('snapchat');
187
-
188
- // WeChat In-App Browser
189
- // UA contains: MicroMessenger
190
- const isWeChatInApp = ua.includes('micromessenger');
191
-
192
- // Threads In-App Browser (Meta's app)
193
- // UA contains: Threads or uses same markers as Instagram
194
- const isThreadsInApp = ua.includes('barcelona'); // Threads codename is Barcelona
195
-
196
- // LinkedIn In-App Browser
197
- // UA contains: LinkedIn - NOTE: Uses Chrome WebView on Android, may support push
198
- const isLinkedInInApp = ua.includes('linkedinapp');
199
-
200
- // Twitter/X In-App Browser
201
- // UA contains: Twitter - NOTE: Uses Chrome WebView on Android, may support push
202
- const isTwitterInApp = ua.includes('twitter');
203
-
204
- // Pinterest In-App Browser
205
- const isPinterestInApp = ua.includes('pinterest');
206
-
207
- // Telegram In-App Browser
208
- const isTelegramInApp = ua.includes('telegram');
209
-
210
- // Line In-App Browser
211
- const isLineInApp = ua.includes('line/');
212
-
213
- // Kakao In-App Browser (Korea)
214
- const isKakaoInApp = ua.includes('kakaotalk');
215
-
216
- // Generic WebView detection
217
- // Android WebView: contains 'wv' in UA or specific WebView markers
218
- // iOS WebView: WKWebView doesn't have distinct UA, but some markers exist
219
- const isWebView = ua.includes('wv)') || // Android WebView marker
220
- ua.includes('webview') ||
221
- ua.includes('; wv') ||
222
- (ua.includes('iphone') && !ua.includes('safari')) || // iOS WebView (no Safari)
223
- (ua.includes('ipad') && !ua.includes('safari'));
224
-
225
- // Aggregate: Any in-app browser
226
- const isInAppBrowser = isFacebookInApp ||
227
- isInstagramInApp ||
228
- isTikTokInApp ||
229
- isSnapchatInApp ||
230
- isWeChatInApp ||
231
- isThreadsInApp ||
232
- isLinkedInInApp ||
233
- isTwitterInApp ||
234
- isPinterestInApp ||
235
- isTelegramInApp ||
236
- isLineInApp ||
237
- isKakaoInApp;
238
-
239
- // iOS detection (all iOS browsers use WebKit, limited push support)
240
- const isIOSBrowser = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod');
241
-
242
- // Opera (modern Chromium-based)
243
- const isOpera = (ua.includes('opr/') || ua.includes('opera')) && !isOperaMini;
244
-
245
- // Chrome (not Edge, not other Chromium browsers)
246
- const isChrome = ua.includes('chrome') &&
247
- !isEdge &&
248
- !isOpera &&
249
- !isYandex &&
250
- !isSamsungBrowser &&
251
- !isVivaldi &&
252
- !isArc &&
253
- !isBrave &&
254
- !isComet;
255
-
256
- // Firefox
257
- const isFirefox = ua.includes('firefox') && !ua.includes('seamonkey');
258
-
259
- // Safari (real Safari, not Chromium pretending to be Safari)
260
- // Safari will have 'safari' in UA but NOT 'chrome' or 'chromium'
261
- // Real Safari uses WebKit engine
262
- // Additional check: Safari has 'version/' in UA, Chromium browsers don't combine it with Safari
263
- const hasSafariUA = ua.includes('safari');
264
- const hasChrome = ua.includes('chrome') || ua.includes('crios');
265
- const hasVersion = ua.includes('version/'); // Real Safari includes Version/XX.X
266
- const isSafari = hasSafariUA && !hasChrome && hasVersion;
267
-
268
- // Chromium detection (any browser using Chromium/Blink engine)
269
- // If it has "chrome" in UA or is one of the known Chromium browsers
270
- const isChromium = hasChrome ||
271
- isEdge ||
272
- isOpera ||
273
- isYandex ||
274
- isSamsungBrowser ||
275
- isVivaldi ||
276
- isArc ||
277
- isBrave ||
278
- isUCBrowser ||
279
- isComet;
280
-
281
- // Engine detection
282
- const isWebKit = !isChromium && isSafari;
283
- const isBlink = isChromium;
284
- const isGecko = isFirefox;
285
-
286
- // Determine browser name
287
- // In-app browsers take priority in naming
288
- let browserName = 'unknown';
289
- if (isFacebookInApp) browserName = 'Facebook In-App';
290
- else if (isInstagramInApp) browserName = 'Instagram In-App';
291
- else if (isTikTokInApp) browserName = 'TikTok In-App';
292
- else if (isSnapchatInApp) browserName = 'Snapchat In-App';
293
- else if (isWeChatInApp) browserName = 'WeChat In-App';
294
- else if (isThreadsInApp) browserName = 'Threads In-App';
295
- else if (isLinkedInInApp) browserName = 'LinkedIn In-App';
296
- else if (isTwitterInApp) browserName = 'Twitter In-App';
297
- else if (isPinterestInApp) browserName = 'Pinterest In-App';
298
- else if (isTelegramInApp) browserName = 'Telegram In-App';
299
- else if (isLineInApp) browserName = 'Line In-App';
300
- else if (isKakaoInApp) browserName = 'KakaoTalk In-App';
301
- else if (isComet) browserName = 'Comet';
302
- else if (isOperaMini) browserName = 'Opera Mini';
303
- else if (isIE) browserName = 'Internet Explorer';
304
- else if (isBrave) browserName = 'Brave';
305
- else if (isArc) browserName = 'Arc';
306
- else if (isVivaldi) browserName = 'Vivaldi';
307
- else if (isYandex) browserName = 'Yandex';
308
- else if (isSamsungBrowser) browserName = 'Samsung Internet';
309
- else if (isUCBrowser) browserName = 'UC Browser';
310
- else if (isEdge) browserName = 'Edge';
311
- else if (isOpera) browserName = 'Opera';
312
- else if (isChrome) browserName = 'Chrome';
313
- else if (isSafari) browserName = 'Safari';
314
- else if (isFirefox) browserName = 'Firefox';
315
- else if (isWebView) browserName = 'WebView';
316
-
317
- // Determine push notification support
318
- // These browsers are known to NOT support Web Push:
319
- // - Opera Mini: no service worker support
320
- // - Internet Explorer: deprecated, no Push API
321
- // - UC Browser: unreliable push support on many versions
322
- // - Most In-App browsers (Facebook, Instagram, TikTok, etc.)
323
- // - Generic WebViews (except Twitter/LinkedIn on Android)
324
- //
325
- // Note: Comet (Perplexity) is Chromium-based and DOES support push notifications.
326
- // Note: Twitter and LinkedIn on Android use Chrome WebView and DO support push.
327
- // However, we err on the side of caution and disable for all in-app browsers.
328
- const browserBlocksPush = isOperaMini ||
329
- isIE ||
330
- isUCBrowser ||
331
- isFacebookInApp ||
332
- isInstagramInApp ||
333
- isTikTokInApp ||
334
- isSnapchatInApp ||
335
- isWeChatInApp ||
336
- isThreadsInApp ||
337
- isPinterestInApp ||
338
- isTelegramInApp ||
339
- isLineInApp ||
340
- isKakaoInApp ||
341
- isWebView;
342
-
343
- // Twitter and LinkedIn on Android use Chrome WebView - they actually support push
344
- // But only on Android, not iOS
345
- const twitterLinkedInAndroid = (isTwitterInApp || isLinkedInInApp) && !isIOSBrowser;
346
-
347
- const supportsPushNotifications = !browserBlocksPush || twitterLinkedInAndroid;
348
-
349
- return {
350
- isChrome,
351
- isChromium,
352
- isSafari,
353
- isFirefox,
354
- isEdge,
355
- isOpera,
356
- isBrave,
357
- isArc,
358
- isVivaldi,
359
- isYandex,
360
- isSamsungBrowser,
361
- isUCBrowser,
362
- isComet,
363
- isOperaMini,
364
- isIE,
365
- isFacebookInApp,
366
- isInstagramInApp,
367
- isTikTokInApp,
368
- isSnapchatInApp,
369
- isWeChatInApp,
370
- isThreadsInApp,
371
- isLinkedInInApp,
372
- isTwitterInApp,
373
- isInAppBrowser,
374
- isWebView,
375
- browserName,
376
- isWebKit,
377
- isBlink,
378
- isGecko,
379
- supportsPushNotifications,
380
- isIOSBrowser,
381
- userAgent: window.navigator.userAgent,
382
- };
383
- }, []);
384
- }
@@ -1,270 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect, useMemo, useState } from 'react';
4
-
5
- // Safe defaults for SSR
6
- const defaultSelectors = {
7
- isMobile: false,
8
- isTablet: false,
9
- isDesktop: false,
10
- isBrowser: false,
11
- isMobileOnly: false,
12
- isSmartTV: false,
13
- isConsole: false,
14
- isWearable: false,
15
- isEmbedded: false,
16
- isAndroid: false,
17
- isIOS: false,
18
- isWindows: false,
19
- isMacOs: false,
20
- isWinPhone: false,
21
- isChrome: false,
22
- isFirefox: false,
23
- isSafari: false,
24
- isOpera: false,
25
- isIE: false,
26
- isEdge: false,
27
- isEdgeChromium: false,
28
- isLegacyEdge: false,
29
- isChromium: false,
30
- isMobileSafari: false,
31
- isYandex: false,
32
- isMIUI: false,
33
- isSamsungBrowser: false,
34
- isElectron: false,
35
- osVersion: 'unknown',
36
- osName: 'unknown',
37
- fullBrowserVersion: 'unknown',
38
- browserVersion: 'unknown',
39
- browserName: 'unknown',
40
- mobileVendor: 'unknown',
41
- mobileModel: 'unknown',
42
- engineName: 'unknown',
43
- engineVersion: 'unknown',
44
- getUA: '',
45
- deviceType: 'unknown',
46
- isIOS13: false,
47
- isIPad13: false,
48
- isIPhone13: false,
49
- isIPod13: false,
50
- };
51
-
52
- const defaultDeviceData = {
53
- deviceType: 'unknown',
54
- osName: 'unknown',
55
- osVersion: 'unknown',
56
- browserName: 'unknown',
57
- browserVersion: 'unknown',
58
- fullBrowserVersion: 'unknown',
59
- mobileVendor: 'unknown',
60
- mobileModel: 'unknown',
61
- engineName: 'unknown',
62
- engineVersion: 'unknown',
63
- getUA: '',
64
- };
65
-
66
- const defaultOrientation = {
67
- isPortrait: false,
68
- isLandscape: false,
69
- orientation: 'portrait' as 'portrait' | 'landscape',
70
- };
71
-
72
- /**
73
- * Device detection hook wrapper for react-device-detect
74
- *
75
- * Provides a convenient interface to access device information including:
76
- * - Device type (mobile, tablet, desktop, etc.)
77
- * - Browser information (name, version, etc.)
78
- * - OS information (name, version, etc.)
79
- * - Orientation (portrait/landscape)
80
- *
81
- * @param userAgent - Optional user agent string (useful for SSR)
82
- * @returns Device detection object with all available information
83
- *
84
- * @example
85
- * ```tsx
86
- * const device = useDeviceDetect();
87
- *
88
- * if (device.isMobile) {
89
- * return <MobileView />;
90
- * }
91
- *
92
- * return <DesktopView />;
93
- * ```
94
- */
95
- export function useDeviceDetect(userAgent?: string) {
96
- const [deviceInfo, setDeviceInfo] = useState<{
97
- selectors: typeof defaultSelectors;
98
- deviceData: typeof defaultDeviceData;
99
- orientation: {
100
- isPortrait: boolean;
101
- isLandscape: boolean;
102
- orientation: 'portrait' | 'landscape';
103
- };
104
- }>({
105
- selectors: defaultSelectors,
106
- deviceData: defaultDeviceData,
107
- orientation: defaultOrientation,
108
- });
109
-
110
- useEffect(() => {
111
- // Only run on client side
112
- if (typeof window === 'undefined') return;
113
-
114
- // Dynamic import to avoid SSR issues
115
- import('react-device-detect').then((deviceDetect) => {
116
- // Get user agent string
117
- const ua = userAgent || (typeof window !== 'undefined' ? window.navigator.userAgent : '');
118
-
119
- if (!ua) {
120
- console.warn('No user agent available');
121
- return;
122
- }
123
-
124
- // Parse user agent using library's parseUserAgent function
125
- const parsed = deviceDetect.parseUserAgent(ua);
126
-
127
- if (!parsed) {
128
- console.warn('Failed to parse user agent');
129
- return;
130
- }
131
-
132
- // Build selectors using library's buildSelectorsObject
133
- // We need to import buildSelectorsObject, but it's not exported
134
- // So we'll use getSelectorsByUserAgent which is exported
135
- const selectors = deviceDetect.getSelectorsByUserAgent(ua) || defaultSelectors;
136
-
137
- // Extract device data from parsed result
138
- const deviceData = {
139
- deviceType: parsed.device?.type || 'unknown',
140
- osName: parsed.os?.name || 'unknown',
141
- osVersion: parsed.os?.version || 'unknown',
142
- browserName: parsed.browser?.name || 'unknown',
143
- browserVersion: parsed.browser?.version || 'unknown',
144
- fullBrowserVersion: parsed.browser?.version || 'unknown',
145
- mobileVendor: parsed.device?.vendor || 'unknown',
146
- mobileModel: parsed.device?.model || 'unknown',
147
- engineName: parsed.engine?.name || 'unknown',
148
- engineVersion: parsed.engine?.version || 'unknown',
149
- getUA: parsed.ua || ua,
150
- };
151
-
152
- // Get orientation - use library's hook if available, otherwise calculate
153
- let orientation = defaultOrientation;
154
-
155
- try {
156
- // Try to use the hook, but we can't call hooks conditionally
157
- // So we'll calculate orientation manually
158
- if (typeof window !== 'undefined') {
159
- const isPortrait = window.innerHeight > window.innerWidth;
160
- orientation = {
161
- isPortrait,
162
- isLandscape: !isPortrait,
163
- orientation: (isPortrait ? 'portrait' : 'landscape') as 'portrait' | 'landscape',
164
- };
165
- }
166
- } catch (error) {
167
- console.warn('Failed to get orientation:', error);
168
- }
169
-
170
- setDeviceInfo({ selectors, deviceData, orientation });
171
- }).catch((error) => {
172
- console.warn('Failed to load device detection:', error);
173
- });
174
- }, [userAgent]);
175
-
176
- // Update orientation on window resize
177
- useEffect(() => {
178
- if (typeof window === 'undefined') return;
179
-
180
- const handleResize = () => {
181
- const isPortrait = window.innerHeight > window.innerWidth;
182
- setDeviceInfo((prev) => ({
183
- ...prev,
184
- orientation: {
185
- isPortrait,
186
- isLandscape: !isPortrait,
187
- orientation: (isPortrait ? 'portrait' : 'landscape') as 'portrait' | 'landscape',
188
- },
189
- }));
190
- };
191
-
192
- window.addEventListener('resize', handleResize);
193
- window.addEventListener('orientationchange', handleResize);
194
-
195
- return () => {
196
- window.removeEventListener('resize', handleResize);
197
- window.removeEventListener('orientationchange', handleResize);
198
- };
199
- }, []);
200
-
201
- const { selectors, deviceData, orientation } = deviceInfo;
202
-
203
- return useMemo(() => {
204
- return {
205
- // Device type selectors
206
- isMobile: selectors.isMobile ?? false,
207
- isTablet: selectors.isTablet ?? false,
208
- isDesktop: selectors.isDesktop ?? false,
209
- isBrowser: selectors.isBrowser ?? false,
210
- isMobileOnly: selectors.isMobileOnly ?? false,
211
- isSmartTV: selectors.isSmartTV ?? false,
212
- isConsole: selectors.isConsole ?? false,
213
- isWearable: selectors.isWearable ?? false,
214
- isEmbedded: selectors.isEmbedded ?? false,
215
-
216
- // OS selectors
217
- isAndroid: selectors.isAndroid ?? false,
218
- isIOS: selectors.isIOS ?? false,
219
- isWindows: selectors.isWindows ?? false,
220
- isMacOs: selectors.isMacOs ?? false,
221
- isWinPhone: selectors.isWinPhone ?? false,
222
-
223
- // Browser selectors
224
- isChrome: selectors.isChrome ?? false,
225
- isFirefox: selectors.isFirefox ?? false,
226
- isSafari: selectors.isSafari ?? false,
227
- isOpera: selectors.isOpera ?? false,
228
- isIE: selectors.isIE ?? false,
229
- isEdge: selectors.isEdge ?? false,
230
- isEdgeChromium: selectors.isEdgeChromium ?? false,
231
- isLegacyEdge: selectors.isLegacyEdge ?? false,
232
- isChromium: selectors.isChromium ?? false,
233
- isMobileSafari: selectors.isMobileSafari ?? false,
234
- isYandex: selectors.isYandex ?? false,
235
- isMIUI: selectors.isMIUI ?? false,
236
- isSamsungBrowser: selectors.isSamsungBrowser ?? false,
237
- isElectron: selectors.isElectron ?? false,
238
-
239
- // iOS version selectors
240
- isIOS13: selectors.isIOS13 ?? false,
241
- isIPad13: selectors.isIPad13 ?? false,
242
- isIPhone13: selectors.isIPhone13 ?? false,
243
- isIPod13: selectors.isIPod13 ?? false,
244
-
245
- // Device information
246
- deviceType: deviceData.deviceType ?? 'unknown',
247
- osName: deviceData.osName ?? 'unknown',
248
- osVersion: deviceData.osVersion ?? 'unknown',
249
- browserName: deviceData.browserName ?? 'unknown',
250
- browserVersion: deviceData.browserVersion ?? 'unknown',
251
- fullBrowserVersion: deviceData.fullBrowserVersion ?? 'unknown',
252
- mobileVendor: deviceData.mobileVendor ?? 'unknown',
253
- mobileModel: deviceData.mobileModel ?? 'unknown',
254
- engineName: deviceData.engineName ?? 'unknown',
255
- engineVersion: deviceData.engineVersion ?? 'unknown',
256
- getUA: deviceData.getUA ?? '',
257
-
258
- // Orientation
259
- isPortrait: orientation.isPortrait,
260
- isLandscape: orientation.isLandscape,
261
- orientation: orientation.orientation,
262
-
263
- // Raw data (for advanced usage)
264
- selectors,
265
- deviceData,
266
- };
267
- }, [selectors, deviceData, orientation]);
268
- }
269
-
270
- export type DeviceDetectResult = ReturnType<typeof useDeviceDetect>;
@@ -1,104 +0,0 @@
1
- 'use client';
2
-
3
- import type { RefObject } from 'react';
4
- import { Options as HotkeysOptions, useHotkeys } from 'react-hotkeys-hook';
5
-
6
- import type { HotkeyCallback, Keys } from 'react-hotkeys-hook';
7
-
8
- /**
9
- * Options for the useHotkey hook
10
- */
11
- export interface UseHotkeyOptions extends Omit<HotkeysOptions, 'enabled'> {
12
- /** Whether the hotkey is enabled (default: true) */
13
- enabled?: boolean;
14
- /** Scope for the hotkey - useful for context-specific shortcuts */
15
- scope?: string;
16
- /** Only trigger when focus is within a specific element */
17
- scopes?: string[];
18
- /** Prevent default browser behavior */
19
- preventDefault?: boolean;
20
- /** Enable in input fields and textareas */
21
- enableOnFormTags?: boolean | readonly ('input' | 'textarea' | 'select')[];
22
- /** Enable when contentEditable element is focused */
23
- enableOnContentEditable?: boolean;
24
- /** Split key for multiple hotkey combinations (default: ',') */
25
- splitKey?: string;
26
- /** Key up/down events */
27
- keyup?: boolean;
28
- keydown?: boolean;
29
- /** Description for the hotkey (useful for help dialogs) */
30
- description?: string;
31
- }
32
-
33
- /**
34
- * Simple wrapper hook for react-hotkeys-hook
35
- *
36
- * @example
37
- * // Single key
38
- * useHotkey('escape', () => closeModal());
39
- *
40
- * @example
41
- * // Key combination
42
- * useHotkey('ctrl+s', (e) => {
43
- * e.preventDefault();
44
- * saveDocument();
45
- * });
46
- *
47
- * @example
48
- * // Multiple keys (any of them will trigger)
49
- * useHotkey(['ArrowLeft', '['], () => goToPrevious());
50
- * useHotkey(['ArrowRight', ']'], () => goToNext());
51
- *
52
- * @example
53
- * // With options
54
- * useHotkey('/', () => focusSearch(), {
55
- * preventDefault: true,
56
- * enableOnFormTags: false,
57
- * description: 'Focus search input'
58
- * });
59
- *
60
- * @example
61
- * // Scoped hotkeys
62
- * useHotkey('delete', () => deleteItem(), { scopes: ['list-view'] });
63
- *
64
- * @param keys - Hotkey or array of hotkeys (e.g., 'ctrl+s', 'ArrowLeft', ['[', 'ArrowLeft'])
65
- * @param callback - Function to call when hotkey is pressed
66
- * @param options - Configuration options
67
- * @returns Ref to attach to element for scoped hotkeys
68
- */
69
- export function useHotkey<T extends HTMLElement = HTMLElement>(
70
- keys: Keys,
71
- callback: HotkeyCallback,
72
- options: UseHotkeyOptions = {}
73
- ): RefObject<T | null> {
74
- const {
75
- enabled = true,
76
- preventDefault = false,
77
- enableOnFormTags = false,
78
- enableOnContentEditable = false,
79
- description: _description,
80
- ...restOptions
81
- } = options;
82
-
83
- return useHotkeys<T>(
84
- keys,
85
- (event, handler) => {
86
- if (preventDefault) {
87
- event.preventDefault();
88
- }
89
- callback(event, handler);
90
- },
91
- {
92
- enabled,
93
- enableOnFormTags,
94
- enableOnContentEditable,
95
- ...restOptions,
96
- }
97
- );
98
- }
99
-
100
- // Re-export useful utilities from react-hotkeys-hook
101
- export { useHotkeysContext, HotkeysProvider, isHotkeyPressed } from 'react-hotkeys-hook';
102
-
103
- // Re-export types
104
- export type { HotkeyCallback, Keys } from 'react-hotkeys-hook';