@opensite/hooks 2.0.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tailwind CSS breakpoint identifiers.
3
+ *
4
+ * - `"default"` – viewport width below the smallest defined breakpoint (< 640px)
5
+ * - `"sm"` – small devices (>= 640px)
6
+ * - `"md"` – medium devices (>= 768px)
7
+ * - `"lg"` – large devices (>= 1024px)
8
+ * - `"xl"` – extra large devices (>= 1280px)
9
+ * - `"2xl"` – 2x extra large devices (>= 1536px)
10
+ */
11
+ export type TailwindSize = "default" | "sm" | "md" | "lg" | "xl" | "2xl";
12
+ /**
13
+ * Semantic screen type classification for layout decisions.
14
+ *
15
+ * - `"MOBILE"` – phone-sized viewports (default + sm breakpoints)
16
+ * - `"TABLET"` – tablet-sized viewports (md breakpoint)
17
+ * - `"DESKTOP"` – desktop-sized viewports (lg, xl, 2xl breakpoints)
18
+ * - `"UNKNOWN"` – returned during SSR before client-side detection
19
+ */
20
+ export type ScreenType = "UNKNOWN" | "MOBILE" | "TABLET" | "DESKTOP";
21
+ /**
22
+ * Breakpoint configuration with pixel thresholds.
23
+ *
24
+ * Values represent the minimum width (in pixels) for each breakpoint.
25
+ * Follows Tailwind CSS v4 defaults.
26
+ */
27
+ export interface ScreenBreakpoints {
28
+ /** Minimum width for `sm` breakpoint. Default: 640 */
29
+ sm: number;
30
+ /** Minimum width for `md` breakpoint. Default: 768 */
31
+ md: number;
32
+ /** Minimum width for `lg` breakpoint. Default: 1024 */
33
+ lg: number;
34
+ /** Minimum width for `xl` breakpoint. Default: 1280 */
35
+ xl: number;
36
+ /** Minimum width for `2xl` breakpoint. Default: 1536 */
37
+ "2xl": number;
38
+ }
39
+ /**
40
+ * Mapping of Tailwind breakpoints to semantic screen types.
41
+ */
42
+ export interface ScreenTypeMapping {
43
+ /** Screen type for default (< sm) viewport. Default: "MOBILE" */
44
+ default: ScreenType;
45
+ /** Screen type for sm viewport. Default: "MOBILE" */
46
+ sm: ScreenType;
47
+ /** Screen type for md viewport. Default: "TABLET" */
48
+ md: ScreenType;
49
+ /** Screen type for lg viewport. Default: "DESKTOP" */
50
+ lg: ScreenType;
51
+ /** Screen type for xl viewport. Default: "DESKTOP" */
52
+ xl: ScreenType;
53
+ /** Screen type for 2xl viewport. Default: "DESKTOP" */
54
+ "2xl": ScreenType;
55
+ }
56
+ /**
57
+ * Configuration options for {@link useScreen}.
58
+ */
59
+ export interface UseScreenOptions {
60
+ /**
61
+ * Custom breakpoint pixel thresholds.
62
+ * Partial object that merges with Tailwind v4 defaults.
63
+ */
64
+ breakpoints?: Partial<ScreenBreakpoints>;
65
+ /**
66
+ * Custom mapping of breakpoints to screen types.
67
+ * Partial object that merges with defaults.
68
+ */
69
+ screenTypeMapping?: Partial<ScreenTypeMapping>;
70
+ /**
71
+ * Default screen type returned during SSR and before detection.
72
+ * Defaults to `"UNKNOWN"`.
73
+ */
74
+ defaultScreenType?: ScreenType;
75
+ /**
76
+ * Default Tailwind size returned during SSR and before detection.
77
+ * Defaults to `"default"`.
78
+ */
79
+ defaultTailwindSize?: TailwindSize;
80
+ }
81
+ /**
82
+ * Shape returned by {@link useScreen}.
83
+ *
84
+ * The object reference is memoized with `useMemo` so it remains referentially
85
+ * stable across re-renders when values have not changed.
86
+ */
87
+ export interface UseScreenResult {
88
+ /** Current viewport width in pixels. `0` during SSR. */
89
+ width: number;
90
+ /** Current viewport height in pixels. `0` during SSR. */
91
+ height: number;
92
+ /**
93
+ * Current Tailwind CSS breakpoint identifier.
94
+ * Determined by the largest matching `min-width` breakpoint.
95
+ */
96
+ tailwindSize: TailwindSize;
97
+ /**
98
+ * Semantic screen type for layout decisions.
99
+ * Maps `tailwindSize` to a simplified classification.
100
+ */
101
+ screenType: ScreenType;
102
+ /** Re-measure viewport dimensions on demand. */
103
+ refresh: () => void;
104
+ }
105
+ /**
106
+ * Track viewport dimensions and compute Tailwind breakpoint / screen type.
107
+ *
108
+ * Provides real-time viewport width and height, along with derived values for
109
+ * the current Tailwind CSS breakpoint (`tailwindSize`) and a semantic screen
110
+ * type classification (`screenType`) for layout decisions.
111
+ *
112
+ * ### SSR Safety
113
+ *
114
+ * The hook returns safe defaults during SSR (`width: 0`, `height: 0`,
115
+ * `tailwindSize: "default"`, `screenType: "UNKNOWN"`) to prevent hydration
116
+ * mismatches. After mount, values update to reflect the actual viewport.
117
+ *
118
+ * ### Performance
119
+ *
120
+ * - Uses `useMediaQuery` internally for efficient breakpoint detection via
121
+ * CSS media queries (no polling).
122
+ * - Viewport dimensions update on window `resize` events.
123
+ * - The returned object is memoized; values must change for the reference
124
+ * to update.
125
+ *
126
+ * @param options - Optional configuration. See {@link UseScreenOptions}.
127
+ * @returns A memoized {@link UseScreenResult} object.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * import { useScreen } from "@opensite/hooks/useScreen";
132
+ *
133
+ * function ResponsiveLayout() {
134
+ * const { screenType, tailwindSize, width } = useScreen();
135
+ *
136
+ * return (
137
+ * <div>
138
+ * <p>Viewport: {width}px ({tailwindSize})</p>
139
+ * {screenType === "MOBILE" && <MobileNav />}
140
+ * {screenType === "TABLET" && <TabletNav />}
141
+ * {screenType === "DESKTOP" && <DesktopNav />}
142
+ * </div>
143
+ * );
144
+ * }
145
+ * ```
146
+ *
147
+ * @example
148
+ * ```tsx
149
+ * // With custom breakpoints
150
+ * const { screenType } = useScreen({
151
+ * breakpoints: { sm: 600, md: 900, lg: 1200, xl: 1400, "2xl": 1800 },
152
+ * });
153
+ * ```
154
+ */
155
+ export declare function useScreen(options?: UseScreenOptions): UseScreenResult;
@@ -0,0 +1,205 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useMediaQuery } from "./useMediaQuery.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Constants
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Tailwind CSS v4 default breakpoints (min-width values in pixels).
8
+ * @see https://tailwindcss.com/docs/responsive-design
9
+ */
10
+ const DEFAULT_BREAKPOINTS = {
11
+ sm: 640,
12
+ md: 768,
13
+ lg: 1024,
14
+ xl: 1280,
15
+ "2xl": 1536,
16
+ };
17
+ /**
18
+ * Default mapping of Tailwind breakpoints to semantic screen types.
19
+ *
20
+ * - default, sm → MOBILE
21
+ * - md → TABLET
22
+ * - lg, xl, 2xl → DESKTOP
23
+ */
24
+ const DEFAULT_SCREEN_TYPE_MAPPING = {
25
+ default: "MOBILE",
26
+ sm: "MOBILE",
27
+ md: "TABLET",
28
+ lg: "DESKTOP",
29
+ xl: "DESKTOP",
30
+ "2xl": "DESKTOP",
31
+ };
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Get current viewport dimensions.
37
+ * Returns `{ width: 0, height: 0 }` during SSR.
38
+ */
39
+ function getViewportDimensions() {
40
+ if (typeof window === "undefined") {
41
+ return { width: 0, height: 0 };
42
+ }
43
+ return {
44
+ width: window.innerWidth,
45
+ height: window.innerHeight,
46
+ };
47
+ }
48
+ /**
49
+ * Determine the current Tailwind breakpoint based on viewport width.
50
+ * Uses mobile-first logic: returns the largest breakpoint that matches.
51
+ */
52
+ function calculateTailwindSize(width, breakpoints) {
53
+ // Check breakpoints from largest to smallest (mobile-first)
54
+ if (width >= breakpoints["2xl"])
55
+ return "2xl";
56
+ if (width >= breakpoints.xl)
57
+ return "xl";
58
+ if (width >= breakpoints.lg)
59
+ return "lg";
60
+ if (width >= breakpoints.md)
61
+ return "md";
62
+ if (width >= breakpoints.sm)
63
+ return "sm";
64
+ return "default";
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Hook
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Track viewport dimensions and compute Tailwind breakpoint / screen type.
71
+ *
72
+ * Provides real-time viewport width and height, along with derived values for
73
+ * the current Tailwind CSS breakpoint (`tailwindSize`) and a semantic screen
74
+ * type classification (`screenType`) for layout decisions.
75
+ *
76
+ * ### SSR Safety
77
+ *
78
+ * The hook returns safe defaults during SSR (`width: 0`, `height: 0`,
79
+ * `tailwindSize: "default"`, `screenType: "UNKNOWN"`) to prevent hydration
80
+ * mismatches. After mount, values update to reflect the actual viewport.
81
+ *
82
+ * ### Performance
83
+ *
84
+ * - Uses `useMediaQuery` internally for efficient breakpoint detection via
85
+ * CSS media queries (no polling).
86
+ * - Viewport dimensions update on window `resize` events.
87
+ * - The returned object is memoized; values must change for the reference
88
+ * to update.
89
+ *
90
+ * @param options - Optional configuration. See {@link UseScreenOptions}.
91
+ * @returns A memoized {@link UseScreenResult} object.
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * import { useScreen } from "@opensite/hooks/useScreen";
96
+ *
97
+ * function ResponsiveLayout() {
98
+ * const { screenType, tailwindSize, width } = useScreen();
99
+ *
100
+ * return (
101
+ * <div>
102
+ * <p>Viewport: {width}px ({tailwindSize})</p>
103
+ * {screenType === "MOBILE" && <MobileNav />}
104
+ * {screenType === "TABLET" && <TabletNav />}
105
+ * {screenType === "DESKTOP" && <DesktopNav />}
106
+ * </div>
107
+ * );
108
+ * }
109
+ * ```
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * // With custom breakpoints
114
+ * const { screenType } = useScreen({
115
+ * breakpoints: { sm: 600, md: 900, lg: 1200, xl: 1400, "2xl": 1800 },
116
+ * });
117
+ * ```
118
+ */
119
+ export function useScreen(options = {}) {
120
+ const { breakpoints: customBreakpoints, screenTypeMapping: customMapping, defaultScreenType = "UNKNOWN", defaultTailwindSize = "default", } = options;
121
+ // Merge custom config with defaults
122
+ const breakpoints = useMemo(() => ({ ...DEFAULT_BREAKPOINTS, ...customBreakpoints }), [customBreakpoints]);
123
+ const screenTypeMapping = useMemo(() => ({ ...DEFAULT_SCREEN_TYPE_MAPPING, ...customMapping }), [customMapping]);
124
+ // Track viewport dimensions
125
+ const [dimensions, setDimensions] = useState(() => getViewportDimensions());
126
+ // Use media queries for breakpoint detection (more reliable than width alone
127
+ // for edge cases and provides instant updates via matchMedia)
128
+ const isSm = useMediaQuery(`(min-width: ${breakpoints.sm}px)`);
129
+ const isMd = useMediaQuery(`(min-width: ${breakpoints.md}px)`);
130
+ const isLg = useMediaQuery(`(min-width: ${breakpoints.lg}px)`);
131
+ const isXl = useMediaQuery(`(min-width: ${breakpoints.xl}px)`);
132
+ const is2xl = useMediaQuery(`(min-width: ${breakpoints["2xl"]}px)`);
133
+ // Stable refresh callback
134
+ const refresh = useCallback(() => {
135
+ if (typeof window === "undefined")
136
+ return;
137
+ setDimensions(getViewportDimensions());
138
+ }, []);
139
+ // Subscribe to resize events for dimension tracking
140
+ useEffect(() => {
141
+ if (typeof window === "undefined")
142
+ return;
143
+ // Initial measurement
144
+ setDimensions(getViewportDimensions());
145
+ const handleResize = () => {
146
+ setDimensions(getViewportDimensions());
147
+ };
148
+ window.addEventListener("resize", handleResize);
149
+ return () => window.removeEventListener("resize", handleResize);
150
+ }, []);
151
+ // Compute tailwindSize from media query results (most reliable)
152
+ const tailwindSize = useMemo(() => {
153
+ // During SSR, all media queries return false
154
+ if (!isSm && !isMd && !isLg && !isXl && !is2xl) {
155
+ // Check if we're on client with actual dimensions
156
+ if (dimensions.width > 0) {
157
+ return calculateTailwindSize(dimensions.width, breakpoints);
158
+ }
159
+ return defaultTailwindSize;
160
+ }
161
+ // Determine from media queries (largest matching breakpoint wins)
162
+ if (is2xl)
163
+ return "2xl";
164
+ if (isXl)
165
+ return "xl";
166
+ if (isLg)
167
+ return "lg";
168
+ if (isMd)
169
+ return "md";
170
+ if (isSm)
171
+ return "sm";
172
+ return "default";
173
+ }, [
174
+ isSm,
175
+ isMd,
176
+ isLg,
177
+ isXl,
178
+ is2xl,
179
+ dimensions.width,
180
+ breakpoints,
181
+ defaultTailwindSize,
182
+ ]);
183
+ // Derive screen type from tailwind size
184
+ const screenType = useMemo(() => {
185
+ // During SSR before detection
186
+ if (tailwindSize === defaultTailwindSize && dimensions.width === 0) {
187
+ return defaultScreenType;
188
+ }
189
+ return screenTypeMapping[tailwindSize];
190
+ }, [
191
+ tailwindSize,
192
+ screenTypeMapping,
193
+ dimensions.width,
194
+ defaultScreenType,
195
+ defaultTailwindSize,
196
+ ]);
197
+ // Memoize the return object for stable references
198
+ return useMemo(() => ({
199
+ width: dimensions.width,
200
+ height: dimensions.height,
201
+ tailwindSize,
202
+ screenType,
203
+ refresh,
204
+ }), [dimensions.width, dimensions.height, tailwindSize, screenType, refresh]);
205
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensite/hooks",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "Performance-first React hooks for UI state, storage, events, and responsive behavior with tree-shakable exports.",
5
5
  "keywords": [
6
6
  "react",
@@ -73,6 +73,11 @@
73
73
  "require": "./dist/core/useIsClient.cjs",
74
74
  "types": "./dist/core/useIsClient.d.ts"
75
75
  },
76
+ "./useIsTouchDevice": {
77
+ "import": "./dist/core/useIsTouchDevice.js",
78
+ "require": "./dist/core/useIsTouchDevice.cjs",
79
+ "types": "./dist/core/useIsTouchDevice.d.ts"
80
+ },
76
81
  "./useIsomorphicLayoutEffect": {
77
82
  "import": "./dist/core/useIsomorphicLayoutEffect.js",
78
83
  "require": "./dist/core/useIsomorphicLayoutEffect.cjs",
@@ -118,6 +123,11 @@
118
123
  "require": "./dist/core/useResizeObserver.cjs",
119
124
  "types": "./dist/core/useResizeObserver.d.ts"
120
125
  },
126
+ "./useScreen": {
127
+ "import": "./dist/core/useScreen.js",
128
+ "require": "./dist/core/useScreen.cjs",
129
+ "types": "./dist/core/useScreen.d.ts"
130
+ },
121
131
  "./useSessionStorage": {
122
132
  "import": "./dist/core/useSessionStorage.js",
123
133
  "require": "./dist/core/useSessionStorage.cjs",