react-mobile-viewport 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # react-mobile-viewport
2
+
3
+ SSR-safe React hooks that tell you whether the viewport is mobile, tablet, or desktop. Built for **React 18+** and **Next.js App Router**.
4
+
5
+ It uses CSS `matchMedia` (not user-agent sniffing) and React’s `useSyncExternalStore`, so server HTML and the first client render stay in sync.
6
+
7
+ ## Why this approach
8
+
9
+ | Approach | Use it? | Why |
10
+ | --- | --- | --- |
11
+ | `window.matchMedia('(max-width: 767px)')` | Yes | Same source of truth as CSS. Updates only when the breakpoint is crossed. |
12
+ | `window.innerWidth` on every resize | Only if you need exact pixels | Extra renders. Still fine behind `useViewportSize`. |
13
+ | User-Agent parsing | Avoid as the primary signal | Tablets, “Request Desktop Site”, and privacy features make it unreliable. |
14
+ | CSS `@media` only | Best for layout | JS should switch *behavior*, not duplicate your entire layout. |
15
+
16
+ **Hydration rule:** the value rendered on the server must match the first client render. This package does that with `useSyncExternalStore` and an explicit SSR fallback (`false` / `"desktop"` unless you pass a cookie-based hint).
17
+
18
+ ## Install (after you publish)
19
+
20
+ ```bash
21
+ npm install react-mobile-viewport
22
+ ```
23
+
24
+ Peer dependencies: `react` and `react-dom` >= 18.
25
+
26
+ ## Usage
27
+
28
+ ### React (client)
29
+
30
+ ```tsx
31
+ import { useIsMobile } from "react-mobile-viewport";
32
+
33
+ export function Nav() {
34
+ const isMobile = useIsMobile();
35
+
36
+ return isMobile ? <MobileMenu /> : <DesktopMenu />;
37
+ }
38
+ ```
39
+
40
+ Custom breakpoint (widths **below** 1024px count as mobile):
41
+
42
+ ```tsx
43
+ const isMobile = useIsMobile({ breakpoint: 1024 });
44
+ ```
45
+
46
+ ### Breakpoints
47
+
48
+ Default: mobile `< 768`, tablet `768–1023`, desktop `>= 1024`.
49
+
50
+ ```tsx
51
+ import { useBreakpoint } from "react-mobile-viewport";
52
+
53
+ export function LayoutSwitch() {
54
+ const { isMobile, isTablet, isDesktop, breakpoint } = useBreakpoint();
55
+ return <p>{breakpoint}</p>;
56
+ }
57
+ ```
58
+
59
+ ### Arbitrary media queries
60
+
61
+ ```tsx
62
+ import { useMediaQuery } from "react-mobile-viewport";
63
+
64
+ const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
65
+ const isPortrait = useMediaQuery("(orientation: portrait)");
66
+ ```
67
+
68
+ ### Viewport size (only when you need pixels)
69
+
70
+ ```tsx
71
+ import { useViewportSize } from "react-mobile-viewport";
72
+
73
+ const { width, height } = useViewportSize();
74
+ ```
75
+
76
+ Prefer `useIsMobile` / `useBreakpoint` for UI branching. Size updates on every resize.
77
+
78
+ ### Next.js App Router
79
+
80
+ Hooks use the DOM, so call them from a **Client Component**:
81
+
82
+ ```tsx
83
+ "use client";
84
+
85
+ import { useIsMobile } from "react-mobile-viewport";
86
+
87
+ export function DeviceLabel() {
88
+ const isMobile = useIsMobile();
89
+ return <span>{isMobile ? "Mobile" : "Desktop"}</span>;
90
+ }
91
+ ```
92
+
93
+ You can import that component from a Server Component. Do **not** import the hooks directly in a Server Component.
94
+
95
+ ### Optional: avoid a desktop flash on mobile
96
+
97
+ Without a hint, SSR assumes desktop (`false`). Mobile users may see desktop UI for one frame.
98
+
99
+ 1. Set a cookie before paint with the server-safe helper.
100
+ 2. Read it in the root layout and pass it into `ViewportProvider`.
101
+
102
+ `app/layout.tsx`:
103
+
104
+ ```tsx
105
+ import { cookies } from "next/headers";
106
+ import {
107
+ getViewportCookieScript,
108
+ parseViewportCookie,
109
+ } from "react-mobile-viewport/script";
110
+ import { ViewportProvider } from "react-mobile-viewport";
111
+
112
+ export default async function RootLayout({
113
+ children,
114
+ }: {
115
+ children: React.ReactNode;
116
+ }) {
117
+ const cookieStore = await cookies();
118
+ const ssrIsMobile = parseViewportCookie(
119
+ cookieStore.get("viewport")?.value,
120
+ );
121
+
122
+ return (
123
+ <html lang="en">
124
+ <head>
125
+ <script
126
+ dangerouslySetInnerHTML={{ __html: getViewportCookieScript() }}
127
+ />
128
+ </head>
129
+ <body>
130
+ <ViewportProvider ssrIsMobile={ssrIsMobile ?? false}>
131
+ {children}
132
+ </ViewportProvider>
133
+ </body>
134
+ </html>
135
+ );
136
+ }
137
+ ```
138
+
139
+ Import `getViewportCookieScript` from `react-mobile-viewport/script`, not the root entry. The root entry is marked `"use client"`.
140
+
141
+ ## API
142
+
143
+ | Export | Returns | Notes |
144
+ | --- | --- | --- |
145
+ | `useIsMobile({ breakpoint, ssrIsMobile })` | `boolean` | Default breakpoint `768`. |
146
+ | `useBreakpoint({ mobileMaxWidth, tabletMaxWidth, ssrBreakpoint })` | `{ isMobile, isTablet, isDesktop, breakpoint }` | |
147
+ | `useMediaQuery(query, { ssrMatch })` | `boolean` | Primitive hook. |
148
+ | `useViewportSize({ ssrWidth, ssrHeight })` | `{ width, height }` | Resize listener, rAF-batched. |
149
+ | `ViewportProvider` | context | Share breakpoint + SSR defaults. |
150
+ | `getViewportCookieScript()` | `string` | From `/script`. Inline in `<head>`. |
151
+ | `parseViewportCookie(value)` | `boolean \| undefined` | From `/script`. |
152
+
153
+ Hooks work **without** a provider. The provider is only for shared defaults.
154
+
155
+ ## Local development
156
+
157
+ ```bash
158
+ npm install
159
+ npm test
160
+ npm run build
161
+ ```
162
+
163
+ ## Publish to npm — step by step
164
+
165
+ ### 1. Create an npm account
166
+
167
+ Sign up at [https://www.npmjs.com/signup](https://www.npmjs.com/signup) and enable 2FA.
168
+
169
+ ### 2. Pick a unique name
170
+
171
+ ```bash
172
+ npm view react-mobile-viewport
173
+ ```
174
+
175
+ If that prints package info, the name is taken. Change `"name"` in `package.json`. Scoped names are safer:
176
+
177
+ ```json
178
+ {
179
+ "name": "@your-name/react-mobile-viewport"
180
+ }
181
+ ```
182
+
183
+ Fill in `"author"`, `"repository"`, and `"homepage"` before you publish.
184
+
185
+ ### 3. Log in
186
+
187
+ ```bash
188
+ npm login
189
+ ```
190
+
191
+ Confirm with:
192
+
193
+ ```bash
194
+ npm whoami
195
+ ```
196
+
197
+ ### 4. Verify the tarball
198
+
199
+ ```bash
200
+ npm run build
201
+ npm pack --dry-run
202
+ ```
203
+
204
+ You should see `dist/`, `package.json`, `README.md`, and `LICENSE` — not `src/` or `node_modules/`.
205
+
206
+ ### 5. Publish
207
+
208
+ Unscoped public package:
209
+
210
+ ```bash
211
+ npm publish --access public
212
+ ```
213
+
214
+ Scoped package (`@your-name/...`) must use `--access public` unless you have a paid org.
215
+
216
+ `prepublishOnly` already runs typecheck, tests, and build.
217
+
218
+ ### 6. First-time checklist
219
+
220
+ - [ ] Package name is unique
221
+ - [ ] Version is `0.1.0` (or `1.0.0` when you are ready)
222
+ - [ ] README install command matches the name
223
+ - [ ] `npm pack --dry-run` looks right
224
+ - [ ] You are logged in (`npm whoami`)
225
+ - [ ] 2FA code is ready
226
+
227
+ ### 7. Later versions
228
+
229
+ Follow [semver](https://semver.org/):
230
+
231
+ - Bug fix → `0.1.1`
232
+ - New compatible API → `0.2.0`
233
+ - Breaking change → `1.0.0`
234
+
235
+ ```bash
236
+ npm version patch
237
+ npm publish --access public
238
+ ```
239
+
240
+ ### 8. Use it in a Next.js app
241
+
242
+ ```bash
243
+ npm install react-mobile-viewport
244
+ ```
245
+
246
+ If you test locally before publishing:
247
+
248
+ ```bash
249
+ npm pack
250
+ # copies a .tgz next to package.json
251
+ cd /path/to/your-next-app
252
+ npm install /path/to/react-mobile-viewport-0.1.0.tgz
253
+ ```
254
+
255
+ ## Project layout
256
+
257
+ ```
258
+ src/index.ts Client entry (hooks + provider)
259
+ src/script.ts Server-safe cookie helpers
260
+ src/use-is-mobile.ts
261
+ src/use-breakpoint.ts
262
+ src/use-media-query.ts
263
+ src/use-viewport-size.ts
264
+ src/provider.tsx
265
+ tsup.config.ts Dual ESM + CJS build, "use client" banner
266
+ ```
267
+
268
+ ## License
269
+
270
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,158 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/constants.ts
8
+ var DEFAULT_MOBILE_MAX_WIDTH = 768;
9
+ var DEFAULT_TABLET_MAX_WIDTH = 1024;
10
+ var DEFAULT_VIEWPORT_COOKIE = "viewport";
11
+ var defaultConfig = {
12
+ breakpoint: DEFAULT_MOBILE_MAX_WIDTH,
13
+ tabletMaxWidth: DEFAULT_TABLET_MAX_WIDTH,
14
+ ssrIsMobile: false,
15
+ ssrBreakpoint: "desktop",
16
+ ssrWidth: 0,
17
+ ssrHeight: 0
18
+ };
19
+ var ViewportContext = react.createContext(defaultConfig);
20
+ function ViewportProvider({
21
+ children,
22
+ breakpoint = defaultConfig.breakpoint,
23
+ tabletMaxWidth = defaultConfig.tabletMaxWidth,
24
+ ssrIsMobile = defaultConfig.ssrIsMobile,
25
+ ssrBreakpoint = ssrIsMobile ? "mobile" : defaultConfig.ssrBreakpoint,
26
+ ssrWidth = defaultConfig.ssrWidth,
27
+ ssrHeight = defaultConfig.ssrHeight
28
+ }) {
29
+ const value = react.useMemo(
30
+ () => ({
31
+ breakpoint,
32
+ tabletMaxWidth,
33
+ ssrIsMobile,
34
+ ssrBreakpoint,
35
+ ssrWidth,
36
+ ssrHeight
37
+ }),
38
+ [breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight]
39
+ );
40
+ return /* @__PURE__ */ jsxRuntime.jsx(ViewportContext.Provider, { value, children });
41
+ }
42
+ function useViewportConfig() {
43
+ return react.useContext(ViewportContext);
44
+ }
45
+
46
+ // src/media-query.ts
47
+ var subscribeCache = /* @__PURE__ */ new Map();
48
+ function attachMediaListener(mql, onChange) {
49
+ if (typeof mql.addEventListener === "function") {
50
+ mql.addEventListener("change", onChange);
51
+ return () => mql.removeEventListener("change", onChange);
52
+ }
53
+ mql.addListener(onChange);
54
+ return () => mql.removeListener(onChange);
55
+ }
56
+ function getMediaQuerySubscribe(query) {
57
+ const cached = subscribeCache.get(query);
58
+ if (cached) return cached;
59
+ const subscribe = (onStoreChange) => {
60
+ const mql = window.matchMedia(query);
61
+ return attachMediaListener(mql, onStoreChange);
62
+ };
63
+ subscribeCache.set(query, subscribe);
64
+ return subscribe;
65
+ }
66
+ function getMediaQuerySnapshot(query) {
67
+ return window.matchMedia(query).matches;
68
+ }
69
+ function maxWidthQuery(maxWidth) {
70
+ return `(max-width: ${maxWidth - 1}px)`;
71
+ }
72
+ function betweenWidthQuery(minWidth, maxWidth) {
73
+ return `(min-width: ${minWidth}px) and (max-width: ${maxWidth - 1}px)`;
74
+ }
75
+ function useMediaQuery(query, options = {}) {
76
+ const ssrMatch = options.ssrMatch ?? false;
77
+ const subscribe = getMediaQuerySubscribe(query);
78
+ const getSnapshot = react.useCallback(
79
+ () => getMediaQuerySnapshot(query),
80
+ [query]
81
+ );
82
+ const getServerSnapshot = react.useCallback(() => ssrMatch, [ssrMatch]);
83
+ return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
84
+ }
85
+
86
+ // src/use-breakpoint.ts
87
+ function useBreakpoint(options = {}) {
88
+ const config = useViewportConfig();
89
+ const mobileMaxWidth = options.mobileMaxWidth ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
90
+ const tabletMaxWidth = options.tabletMaxWidth ?? config.tabletMaxWidth ?? DEFAULT_TABLET_MAX_WIDTH;
91
+ const ssrBreakpoint = options.ssrBreakpoint ?? config.ssrBreakpoint;
92
+ const isMobile = useMediaQuery(maxWidthQuery(mobileMaxWidth), {
93
+ ssrMatch: ssrBreakpoint === "mobile"
94
+ });
95
+ const isTablet = useMediaQuery(
96
+ betweenWidthQuery(mobileMaxWidth, tabletMaxWidth),
97
+ { ssrMatch: ssrBreakpoint === "tablet" }
98
+ );
99
+ const breakpoint = isMobile ? "mobile" : isTablet ? "tablet" : "desktop";
100
+ return {
101
+ isMobile,
102
+ isTablet,
103
+ isDesktop: breakpoint === "desktop",
104
+ breakpoint
105
+ };
106
+ }
107
+
108
+ // src/use-is-mobile.ts
109
+ function useIsMobile(options = {}) {
110
+ const config = useViewportConfig();
111
+ const breakpoint = options.breakpoint ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
112
+ const ssrIsMobile = options.ssrIsMobile ?? config.ssrIsMobile;
113
+ return useMediaQuery(maxWidthQuery(breakpoint), { ssrMatch: ssrIsMobile });
114
+ }
115
+ var cachedSize = { width: 0, height: 0 };
116
+ function readSize() {
117
+ const width = window.innerWidth;
118
+ const height = window.innerHeight;
119
+ if (cachedSize.width !== width || cachedSize.height !== height) {
120
+ cachedSize = { width, height };
121
+ }
122
+ return cachedSize;
123
+ }
124
+ function subscribeToResize(onStoreChange) {
125
+ let frame = 0;
126
+ const onResize = () => {
127
+ cancelAnimationFrame(frame);
128
+ frame = requestAnimationFrame(onStoreChange);
129
+ };
130
+ window.addEventListener("resize", onResize);
131
+ return () => {
132
+ cancelAnimationFrame(frame);
133
+ window.removeEventListener("resize", onResize);
134
+ };
135
+ }
136
+ function useViewportSize(options = {}) {
137
+ const config = useViewportConfig();
138
+ const ssrWidth = options.ssrWidth ?? config.ssrWidth;
139
+ const ssrHeight = options.ssrHeight ?? config.ssrHeight;
140
+ const serverSnapshot = react.useMemo(
141
+ () => ({ width: ssrWidth, height: ssrHeight }),
142
+ [ssrWidth, ssrHeight]
143
+ );
144
+ const getServerSnapshot = react.useCallback(() => serverSnapshot, [serverSnapshot]);
145
+ return react.useSyncExternalStore(subscribeToResize, readSize, getServerSnapshot);
146
+ }
147
+
148
+ exports.DEFAULT_MOBILE_MAX_WIDTH = DEFAULT_MOBILE_MAX_WIDTH;
149
+ exports.DEFAULT_TABLET_MAX_WIDTH = DEFAULT_TABLET_MAX_WIDTH;
150
+ exports.DEFAULT_VIEWPORT_COOKIE = DEFAULT_VIEWPORT_COOKIE;
151
+ exports.ViewportProvider = ViewportProvider;
152
+ exports.useBreakpoint = useBreakpoint;
153
+ exports.useIsMobile = useIsMobile;
154
+ exports.useMediaQuery = useMediaQuery;
155
+ exports.useViewportConfig = useViewportConfig;
156
+ exports.useViewportSize = useViewportSize;
157
+ //# sourceMappingURL=index.cjs.map
158
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/provider.tsx","../src/media-query.ts","../src/use-media-query.ts","../src/use-breakpoint.ts","../src/use-is-mobile.ts","../src/use-viewport-size.ts"],"names":["createContext","useMemo","jsx","useContext","useCallback","useSyncExternalStore"],"mappings":"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","file":"index.cjs","sourcesContent":["export const DEFAULT_MOBILE_MAX_WIDTH = 768;\nexport const DEFAULT_TABLET_MAX_WIDTH = 1024;\nexport const DEFAULT_VIEWPORT_COOKIE = \"viewport\";\n","import { createContext, useContext, useMemo } from \"react\";\nimport {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_TABLET_MAX_WIDTH,\n} from \"./constants\";\nimport type { ViewportConfig, ViewportProviderProps } from \"./types\";\n\nconst defaultConfig: ViewportConfig = {\n breakpoint: DEFAULT_MOBILE_MAX_WIDTH,\n tabletMaxWidth: DEFAULT_TABLET_MAX_WIDTH,\n ssrIsMobile: false,\n ssrBreakpoint: \"desktop\",\n ssrWidth: 0,\n ssrHeight: 0,\n};\n\nconst ViewportContext = createContext<ViewportConfig>(defaultConfig);\n\nexport function ViewportProvider({\n children,\n breakpoint = defaultConfig.breakpoint,\n tabletMaxWidth = defaultConfig.tabletMaxWidth,\n ssrIsMobile = defaultConfig.ssrIsMobile,\n ssrBreakpoint = ssrIsMobile ? \"mobile\" : defaultConfig.ssrBreakpoint,\n ssrWidth = defaultConfig.ssrWidth,\n ssrHeight = defaultConfig.ssrHeight,\n}: ViewportProviderProps) {\n const value = useMemo(\n (): ViewportConfig => ({\n breakpoint,\n tabletMaxWidth,\n ssrIsMobile,\n ssrBreakpoint,\n ssrWidth,\n ssrHeight,\n }),\n [breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight],\n );\n\n return (\n <ViewportContext.Provider value={value}>{children}</ViewportContext.Provider>\n );\n}\n\nexport function useViewportConfig(): ViewportConfig {\n return useContext(ViewportContext);\n}\n","type Subscribe = (onStoreChange: () => void) => () => void;\n\nconst subscribeCache = new Map<string, Subscribe>();\n\nfunction attachMediaListener(\n mql: MediaQueryList,\n onChange: () => void,\n): () => void {\n if (typeof mql.addEventListener === \"function\") {\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }\n\n mql.addListener(onChange);\n return () => mql.removeListener(onChange);\n}\n\nexport function getMediaQuerySubscribe(query: string): Subscribe {\n const cached = subscribeCache.get(query);\n if (cached) return cached;\n\n const subscribe: Subscribe = (onStoreChange) => {\n const mql = window.matchMedia(query);\n return attachMediaListener(mql, onStoreChange);\n };\n\n subscribeCache.set(query, subscribe);\n return subscribe;\n}\n\nexport function getMediaQuerySnapshot(query: string): boolean {\n return window.matchMedia(query).matches;\n}\n\nexport function maxWidthQuery(maxWidth: number): string {\n return `(max-width: ${maxWidth - 1}px)`;\n}\n\nexport function betweenWidthQuery(minWidth: number, maxWidth: number): string {\n return `(min-width: ${minWidth}px) and (max-width: ${maxWidth - 1}px)`;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\nimport { getMediaQuerySnapshot, getMediaQuerySubscribe } from \"./media-query\";\nimport type { UseMediaQueryOptions } from \"./types\";\n\nexport function useMediaQuery(\n query: string,\n options: UseMediaQueryOptions = {},\n): boolean {\n const ssrMatch = options.ssrMatch ?? false;\n const subscribe = getMediaQuerySubscribe(query);\n const getSnapshot = useCallback(\n () => getMediaQuerySnapshot(query),\n [query],\n );\n const getServerSnapshot = useCallback(() => ssrMatch, [ssrMatch]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_TABLET_MAX_WIDTH,\n} from \"./constants\";\nimport { betweenWidthQuery, maxWidthQuery } from \"./media-query\";\nimport { useViewportConfig } from \"./provider\";\nimport type { BreakpointName, UseBreakpointOptions } from \"./types\";\nimport { useMediaQuery } from \"./use-media-query\";\n\nexport function useBreakpoint(\n options: UseBreakpointOptions = {},\n): {\n isMobile: boolean;\n isTablet: boolean;\n isDesktop: boolean;\n breakpoint: BreakpointName;\n} {\n const config = useViewportConfig();\n const mobileMaxWidth =\n options.mobileMaxWidth ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const tabletMaxWidth =\n options.tabletMaxWidth ?? config.tabletMaxWidth ?? DEFAULT_TABLET_MAX_WIDTH;\n const ssrBreakpoint = options.ssrBreakpoint ?? config.ssrBreakpoint;\n\n const isMobile = useMediaQuery(maxWidthQuery(mobileMaxWidth), {\n ssrMatch: ssrBreakpoint === \"mobile\",\n });\n const isTablet = useMediaQuery(\n betweenWidthQuery(mobileMaxWidth, tabletMaxWidth),\n { ssrMatch: ssrBreakpoint === \"tablet\" },\n );\n\n const breakpoint: BreakpointName = isMobile\n ? \"mobile\"\n : isTablet\n ? \"tablet\"\n : \"desktop\";\n\n return {\n isMobile,\n isTablet,\n isDesktop: breakpoint === \"desktop\",\n breakpoint,\n };\n}\n","import { DEFAULT_MOBILE_MAX_WIDTH } from \"./constants\";\nimport { maxWidthQuery } from \"./media-query\";\nimport { useViewportConfig } from \"./provider\";\nimport type { UseIsMobileOptions } from \"./types\";\nimport { useMediaQuery } from \"./use-media-query\";\n\nexport function useIsMobile(options: UseIsMobileOptions = {}): boolean {\n const config = useViewportConfig();\n const breakpoint = options.breakpoint ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const ssrIsMobile = options.ssrIsMobile ?? config.ssrIsMobile;\n\n return useMediaQuery(maxWidthQuery(breakpoint), { ssrMatch: ssrIsMobile });\n}\n","import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { useViewportConfig } from \"./provider\";\nimport type { UseViewportSizeOptions, ViewportSize } from \"./types\";\n\nlet cachedSize: ViewportSize = { width: 0, height: 0 };\n\nfunction readSize(): ViewportSize {\n const width = window.innerWidth;\n const height = window.innerHeight;\n\n if (cachedSize.width !== width || cachedSize.height !== height) {\n cachedSize = { width, height };\n }\n\n return cachedSize;\n}\n\nfunction subscribeToResize(onStoreChange: () => void): () => void {\n let frame = 0;\n\n const onResize = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(onStoreChange);\n };\n\n window.addEventListener(\"resize\", onResize);\n return () => {\n cancelAnimationFrame(frame);\n window.removeEventListener(\"resize\", onResize);\n };\n}\n\nexport function useViewportSize(\n options: UseViewportSizeOptions = {},\n): ViewportSize {\n const config = useViewportConfig();\n const ssrWidth = options.ssrWidth ?? config.ssrWidth;\n const ssrHeight = options.ssrHeight ?? config.ssrHeight;\n const serverSnapshot = useMemo(\n (): ViewportSize => ({ width: ssrWidth, height: ssrHeight }),\n [ssrWidth, ssrHeight],\n );\n const getServerSnapshot = useCallback(() => serverSnapshot, [serverSnapshot]);\n\n return useSyncExternalStore(subscribeToResize, readSize, getServerSnapshot);\n}\n"]}
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare const DEFAULT_MOBILE_MAX_WIDTH = 768;
5
+ declare const DEFAULT_TABLET_MAX_WIDTH = 1024;
6
+ declare const DEFAULT_VIEWPORT_COOKIE = "viewport";
7
+
8
+ type BreakpointName = "mobile" | "tablet" | "desktop";
9
+ interface UseMediaQueryOptions {
10
+ /** Value used on the server and during hydration. Default: `false`. */
11
+ ssrMatch?: boolean;
12
+ }
13
+ interface UseIsMobileOptions {
14
+ /** Widths below this value (px) are mobile. Default: `768`. */
15
+ breakpoint?: number;
16
+ /** SSR/hydration fallback. Default: `false`. */
17
+ ssrIsMobile?: boolean;
18
+ }
19
+ interface UseBreakpointOptions {
20
+ /** Widths below this value (px) are mobile. Default: `768`. */
21
+ mobileMaxWidth?: number;
22
+ /** Widths below this value (px) and not mobile are tablet. Default: `1024`. */
23
+ tabletMaxWidth?: number;
24
+ /** SSR/hydration fallback. Default: `"desktop"`. */
25
+ ssrBreakpoint?: BreakpointName;
26
+ }
27
+ interface ViewportSize {
28
+ width: number;
29
+ height: number;
30
+ }
31
+ interface UseViewportSizeOptions {
32
+ ssrWidth?: number;
33
+ ssrHeight?: number;
34
+ }
35
+ interface ViewportConfig {
36
+ breakpoint: number;
37
+ tabletMaxWidth: number;
38
+ ssrIsMobile: boolean;
39
+ ssrBreakpoint: BreakpointName;
40
+ ssrWidth: number;
41
+ ssrHeight: number;
42
+ }
43
+ interface ViewportProviderProps extends Partial<ViewportConfig> {
44
+ children: ReactNode;
45
+ }
46
+
47
+ declare function ViewportProvider({ children, breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight, }: ViewportProviderProps): react.JSX.Element;
48
+ declare function useViewportConfig(): ViewportConfig;
49
+
50
+ declare function useBreakpoint(options?: UseBreakpointOptions): {
51
+ isMobile: boolean;
52
+ isTablet: boolean;
53
+ isDesktop: boolean;
54
+ breakpoint: BreakpointName;
55
+ };
56
+
57
+ declare function useIsMobile(options?: UseIsMobileOptions): boolean;
58
+
59
+ declare function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean;
60
+
61
+ declare function useViewportSize(options?: UseViewportSizeOptions): ViewportSize;
62
+
63
+ export { type BreakpointName, DEFAULT_MOBILE_MAX_WIDTH, DEFAULT_TABLET_MAX_WIDTH, DEFAULT_VIEWPORT_COOKIE, type UseBreakpointOptions, type UseIsMobileOptions, type UseMediaQueryOptions, type UseViewportSizeOptions, type ViewportConfig, ViewportProvider, type ViewportProviderProps, type ViewportSize, useBreakpoint, useIsMobile, useMediaQuery, useViewportConfig, useViewportSize };
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare const DEFAULT_MOBILE_MAX_WIDTH = 768;
5
+ declare const DEFAULT_TABLET_MAX_WIDTH = 1024;
6
+ declare const DEFAULT_VIEWPORT_COOKIE = "viewport";
7
+
8
+ type BreakpointName = "mobile" | "tablet" | "desktop";
9
+ interface UseMediaQueryOptions {
10
+ /** Value used on the server and during hydration. Default: `false`. */
11
+ ssrMatch?: boolean;
12
+ }
13
+ interface UseIsMobileOptions {
14
+ /** Widths below this value (px) are mobile. Default: `768`. */
15
+ breakpoint?: number;
16
+ /** SSR/hydration fallback. Default: `false`. */
17
+ ssrIsMobile?: boolean;
18
+ }
19
+ interface UseBreakpointOptions {
20
+ /** Widths below this value (px) are mobile. Default: `768`. */
21
+ mobileMaxWidth?: number;
22
+ /** Widths below this value (px) and not mobile are tablet. Default: `1024`. */
23
+ tabletMaxWidth?: number;
24
+ /** SSR/hydration fallback. Default: `"desktop"`. */
25
+ ssrBreakpoint?: BreakpointName;
26
+ }
27
+ interface ViewportSize {
28
+ width: number;
29
+ height: number;
30
+ }
31
+ interface UseViewportSizeOptions {
32
+ ssrWidth?: number;
33
+ ssrHeight?: number;
34
+ }
35
+ interface ViewportConfig {
36
+ breakpoint: number;
37
+ tabletMaxWidth: number;
38
+ ssrIsMobile: boolean;
39
+ ssrBreakpoint: BreakpointName;
40
+ ssrWidth: number;
41
+ ssrHeight: number;
42
+ }
43
+ interface ViewportProviderProps extends Partial<ViewportConfig> {
44
+ children: ReactNode;
45
+ }
46
+
47
+ declare function ViewportProvider({ children, breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight, }: ViewportProviderProps): react.JSX.Element;
48
+ declare function useViewportConfig(): ViewportConfig;
49
+
50
+ declare function useBreakpoint(options?: UseBreakpointOptions): {
51
+ isMobile: boolean;
52
+ isTablet: boolean;
53
+ isDesktop: boolean;
54
+ breakpoint: BreakpointName;
55
+ };
56
+
57
+ declare function useIsMobile(options?: UseIsMobileOptions): boolean;
58
+
59
+ declare function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean;
60
+
61
+ declare function useViewportSize(options?: UseViewportSizeOptions): ViewportSize;
62
+
63
+ export { type BreakpointName, DEFAULT_MOBILE_MAX_WIDTH, DEFAULT_TABLET_MAX_WIDTH, DEFAULT_VIEWPORT_COOKIE, type UseBreakpointOptions, type UseIsMobileOptions, type UseMediaQueryOptions, type UseViewportSizeOptions, type ViewportConfig, ViewportProvider, type ViewportProviderProps, type ViewportSize, useBreakpoint, useIsMobile, useMediaQuery, useViewportConfig, useViewportSize };
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { createContext, useMemo, useContext, useCallback, useSyncExternalStore } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/constants.ts
6
+ var DEFAULT_MOBILE_MAX_WIDTH = 768;
7
+ var DEFAULT_TABLET_MAX_WIDTH = 1024;
8
+ var DEFAULT_VIEWPORT_COOKIE = "viewport";
9
+ var defaultConfig = {
10
+ breakpoint: DEFAULT_MOBILE_MAX_WIDTH,
11
+ tabletMaxWidth: DEFAULT_TABLET_MAX_WIDTH,
12
+ ssrIsMobile: false,
13
+ ssrBreakpoint: "desktop",
14
+ ssrWidth: 0,
15
+ ssrHeight: 0
16
+ };
17
+ var ViewportContext = createContext(defaultConfig);
18
+ function ViewportProvider({
19
+ children,
20
+ breakpoint = defaultConfig.breakpoint,
21
+ tabletMaxWidth = defaultConfig.tabletMaxWidth,
22
+ ssrIsMobile = defaultConfig.ssrIsMobile,
23
+ ssrBreakpoint = ssrIsMobile ? "mobile" : defaultConfig.ssrBreakpoint,
24
+ ssrWidth = defaultConfig.ssrWidth,
25
+ ssrHeight = defaultConfig.ssrHeight
26
+ }) {
27
+ const value = useMemo(
28
+ () => ({
29
+ breakpoint,
30
+ tabletMaxWidth,
31
+ ssrIsMobile,
32
+ ssrBreakpoint,
33
+ ssrWidth,
34
+ ssrHeight
35
+ }),
36
+ [breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight]
37
+ );
38
+ return /* @__PURE__ */ jsx(ViewportContext.Provider, { value, children });
39
+ }
40
+ function useViewportConfig() {
41
+ return useContext(ViewportContext);
42
+ }
43
+
44
+ // src/media-query.ts
45
+ var subscribeCache = /* @__PURE__ */ new Map();
46
+ function attachMediaListener(mql, onChange) {
47
+ if (typeof mql.addEventListener === "function") {
48
+ mql.addEventListener("change", onChange);
49
+ return () => mql.removeEventListener("change", onChange);
50
+ }
51
+ mql.addListener(onChange);
52
+ return () => mql.removeListener(onChange);
53
+ }
54
+ function getMediaQuerySubscribe(query) {
55
+ const cached = subscribeCache.get(query);
56
+ if (cached) return cached;
57
+ const subscribe = (onStoreChange) => {
58
+ const mql = window.matchMedia(query);
59
+ return attachMediaListener(mql, onStoreChange);
60
+ };
61
+ subscribeCache.set(query, subscribe);
62
+ return subscribe;
63
+ }
64
+ function getMediaQuerySnapshot(query) {
65
+ return window.matchMedia(query).matches;
66
+ }
67
+ function maxWidthQuery(maxWidth) {
68
+ return `(max-width: ${maxWidth - 1}px)`;
69
+ }
70
+ function betweenWidthQuery(minWidth, maxWidth) {
71
+ return `(min-width: ${minWidth}px) and (max-width: ${maxWidth - 1}px)`;
72
+ }
73
+ function useMediaQuery(query, options = {}) {
74
+ const ssrMatch = options.ssrMatch ?? false;
75
+ const subscribe = getMediaQuerySubscribe(query);
76
+ const getSnapshot = useCallback(
77
+ () => getMediaQuerySnapshot(query),
78
+ [query]
79
+ );
80
+ const getServerSnapshot = useCallback(() => ssrMatch, [ssrMatch]);
81
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
82
+ }
83
+
84
+ // src/use-breakpoint.ts
85
+ function useBreakpoint(options = {}) {
86
+ const config = useViewportConfig();
87
+ const mobileMaxWidth = options.mobileMaxWidth ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
88
+ const tabletMaxWidth = options.tabletMaxWidth ?? config.tabletMaxWidth ?? DEFAULT_TABLET_MAX_WIDTH;
89
+ const ssrBreakpoint = options.ssrBreakpoint ?? config.ssrBreakpoint;
90
+ const isMobile = useMediaQuery(maxWidthQuery(mobileMaxWidth), {
91
+ ssrMatch: ssrBreakpoint === "mobile"
92
+ });
93
+ const isTablet = useMediaQuery(
94
+ betweenWidthQuery(mobileMaxWidth, tabletMaxWidth),
95
+ { ssrMatch: ssrBreakpoint === "tablet" }
96
+ );
97
+ const breakpoint = isMobile ? "mobile" : isTablet ? "tablet" : "desktop";
98
+ return {
99
+ isMobile,
100
+ isTablet,
101
+ isDesktop: breakpoint === "desktop",
102
+ breakpoint
103
+ };
104
+ }
105
+
106
+ // src/use-is-mobile.ts
107
+ function useIsMobile(options = {}) {
108
+ const config = useViewportConfig();
109
+ const breakpoint = options.breakpoint ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
110
+ const ssrIsMobile = options.ssrIsMobile ?? config.ssrIsMobile;
111
+ return useMediaQuery(maxWidthQuery(breakpoint), { ssrMatch: ssrIsMobile });
112
+ }
113
+ var cachedSize = { width: 0, height: 0 };
114
+ function readSize() {
115
+ const width = window.innerWidth;
116
+ const height = window.innerHeight;
117
+ if (cachedSize.width !== width || cachedSize.height !== height) {
118
+ cachedSize = { width, height };
119
+ }
120
+ return cachedSize;
121
+ }
122
+ function subscribeToResize(onStoreChange) {
123
+ let frame = 0;
124
+ const onResize = () => {
125
+ cancelAnimationFrame(frame);
126
+ frame = requestAnimationFrame(onStoreChange);
127
+ };
128
+ window.addEventListener("resize", onResize);
129
+ return () => {
130
+ cancelAnimationFrame(frame);
131
+ window.removeEventListener("resize", onResize);
132
+ };
133
+ }
134
+ function useViewportSize(options = {}) {
135
+ const config = useViewportConfig();
136
+ const ssrWidth = options.ssrWidth ?? config.ssrWidth;
137
+ const ssrHeight = options.ssrHeight ?? config.ssrHeight;
138
+ const serverSnapshot = useMemo(
139
+ () => ({ width: ssrWidth, height: ssrHeight }),
140
+ [ssrWidth, ssrHeight]
141
+ );
142
+ const getServerSnapshot = useCallback(() => serverSnapshot, [serverSnapshot]);
143
+ return useSyncExternalStore(subscribeToResize, readSize, getServerSnapshot);
144
+ }
145
+
146
+ export { DEFAULT_MOBILE_MAX_WIDTH, DEFAULT_TABLET_MAX_WIDTH, DEFAULT_VIEWPORT_COOKIE, ViewportProvider, useBreakpoint, useIsMobile, useMediaQuery, useViewportConfig, useViewportSize };
147
+ //# sourceMappingURL=index.js.map
148
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/provider.tsx","../src/media-query.ts","../src/use-media-query.ts","../src/use-breakpoint.ts","../src/use-is-mobile.ts","../src/use-viewport-size.ts"],"names":["useMemo","useCallback","useSyncExternalStore"],"mappings":"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","file":"index.js","sourcesContent":["export const DEFAULT_MOBILE_MAX_WIDTH = 768;\nexport const DEFAULT_TABLET_MAX_WIDTH = 1024;\nexport const DEFAULT_VIEWPORT_COOKIE = \"viewport\";\n","import { createContext, useContext, useMemo } from \"react\";\nimport {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_TABLET_MAX_WIDTH,\n} from \"./constants\";\nimport type { ViewportConfig, ViewportProviderProps } from \"./types\";\n\nconst defaultConfig: ViewportConfig = {\n breakpoint: DEFAULT_MOBILE_MAX_WIDTH,\n tabletMaxWidth: DEFAULT_TABLET_MAX_WIDTH,\n ssrIsMobile: false,\n ssrBreakpoint: \"desktop\",\n ssrWidth: 0,\n ssrHeight: 0,\n};\n\nconst ViewportContext = createContext<ViewportConfig>(defaultConfig);\n\nexport function ViewportProvider({\n children,\n breakpoint = defaultConfig.breakpoint,\n tabletMaxWidth = defaultConfig.tabletMaxWidth,\n ssrIsMobile = defaultConfig.ssrIsMobile,\n ssrBreakpoint = ssrIsMobile ? \"mobile\" : defaultConfig.ssrBreakpoint,\n ssrWidth = defaultConfig.ssrWidth,\n ssrHeight = defaultConfig.ssrHeight,\n}: ViewportProviderProps) {\n const value = useMemo(\n (): ViewportConfig => ({\n breakpoint,\n tabletMaxWidth,\n ssrIsMobile,\n ssrBreakpoint,\n ssrWidth,\n ssrHeight,\n }),\n [breakpoint, tabletMaxWidth, ssrIsMobile, ssrBreakpoint, ssrWidth, ssrHeight],\n );\n\n return (\n <ViewportContext.Provider value={value}>{children}</ViewportContext.Provider>\n );\n}\n\nexport function useViewportConfig(): ViewportConfig {\n return useContext(ViewportContext);\n}\n","type Subscribe = (onStoreChange: () => void) => () => void;\n\nconst subscribeCache = new Map<string, Subscribe>();\n\nfunction attachMediaListener(\n mql: MediaQueryList,\n onChange: () => void,\n): () => void {\n if (typeof mql.addEventListener === \"function\") {\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }\n\n mql.addListener(onChange);\n return () => mql.removeListener(onChange);\n}\n\nexport function getMediaQuerySubscribe(query: string): Subscribe {\n const cached = subscribeCache.get(query);\n if (cached) return cached;\n\n const subscribe: Subscribe = (onStoreChange) => {\n const mql = window.matchMedia(query);\n return attachMediaListener(mql, onStoreChange);\n };\n\n subscribeCache.set(query, subscribe);\n return subscribe;\n}\n\nexport function getMediaQuerySnapshot(query: string): boolean {\n return window.matchMedia(query).matches;\n}\n\nexport function maxWidthQuery(maxWidth: number): string {\n return `(max-width: ${maxWidth - 1}px)`;\n}\n\nexport function betweenWidthQuery(minWidth: number, maxWidth: number): string {\n return `(min-width: ${minWidth}px) and (max-width: ${maxWidth - 1}px)`;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\nimport { getMediaQuerySnapshot, getMediaQuerySubscribe } from \"./media-query\";\nimport type { UseMediaQueryOptions } from \"./types\";\n\nexport function useMediaQuery(\n query: string,\n options: UseMediaQueryOptions = {},\n): boolean {\n const ssrMatch = options.ssrMatch ?? false;\n const subscribe = getMediaQuerySubscribe(query);\n const getSnapshot = useCallback(\n () => getMediaQuerySnapshot(query),\n [query],\n );\n const getServerSnapshot = useCallback(() => ssrMatch, [ssrMatch]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_TABLET_MAX_WIDTH,\n} from \"./constants\";\nimport { betweenWidthQuery, maxWidthQuery } from \"./media-query\";\nimport { useViewportConfig } from \"./provider\";\nimport type { BreakpointName, UseBreakpointOptions } from \"./types\";\nimport { useMediaQuery } from \"./use-media-query\";\n\nexport function useBreakpoint(\n options: UseBreakpointOptions = {},\n): {\n isMobile: boolean;\n isTablet: boolean;\n isDesktop: boolean;\n breakpoint: BreakpointName;\n} {\n const config = useViewportConfig();\n const mobileMaxWidth =\n options.mobileMaxWidth ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const tabletMaxWidth =\n options.tabletMaxWidth ?? config.tabletMaxWidth ?? DEFAULT_TABLET_MAX_WIDTH;\n const ssrBreakpoint = options.ssrBreakpoint ?? config.ssrBreakpoint;\n\n const isMobile = useMediaQuery(maxWidthQuery(mobileMaxWidth), {\n ssrMatch: ssrBreakpoint === \"mobile\",\n });\n const isTablet = useMediaQuery(\n betweenWidthQuery(mobileMaxWidth, tabletMaxWidth),\n { ssrMatch: ssrBreakpoint === \"tablet\" },\n );\n\n const breakpoint: BreakpointName = isMobile\n ? \"mobile\"\n : isTablet\n ? \"tablet\"\n : \"desktop\";\n\n return {\n isMobile,\n isTablet,\n isDesktop: breakpoint === \"desktop\",\n breakpoint,\n };\n}\n","import { DEFAULT_MOBILE_MAX_WIDTH } from \"./constants\";\nimport { maxWidthQuery } from \"./media-query\";\nimport { useViewportConfig } from \"./provider\";\nimport type { UseIsMobileOptions } from \"./types\";\nimport { useMediaQuery } from \"./use-media-query\";\n\nexport function useIsMobile(options: UseIsMobileOptions = {}): boolean {\n const config = useViewportConfig();\n const breakpoint = options.breakpoint ?? config.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const ssrIsMobile = options.ssrIsMobile ?? config.ssrIsMobile;\n\n return useMediaQuery(maxWidthQuery(breakpoint), { ssrMatch: ssrIsMobile });\n}\n","import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { useViewportConfig } from \"./provider\";\nimport type { UseViewportSizeOptions, ViewportSize } from \"./types\";\n\nlet cachedSize: ViewportSize = { width: 0, height: 0 };\n\nfunction readSize(): ViewportSize {\n const width = window.innerWidth;\n const height = window.innerHeight;\n\n if (cachedSize.width !== width || cachedSize.height !== height) {\n cachedSize = { width, height };\n }\n\n return cachedSize;\n}\n\nfunction subscribeToResize(onStoreChange: () => void): () => void {\n let frame = 0;\n\n const onResize = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(onStoreChange);\n };\n\n window.addEventListener(\"resize\", onResize);\n return () => {\n cancelAnimationFrame(frame);\n window.removeEventListener(\"resize\", onResize);\n };\n}\n\nexport function useViewportSize(\n options: UseViewportSizeOptions = {},\n): ViewportSize {\n const config = useViewportConfig();\n const ssrWidth = options.ssrWidth ?? config.ssrWidth;\n const ssrHeight = options.ssrHeight ?? config.ssrHeight;\n const serverSnapshot = useMemo(\n (): ViewportSize => ({ width: ssrWidth, height: ssrHeight }),\n [ssrWidth, ssrHeight],\n );\n const getServerSnapshot = useCallback(() => serverSnapshot, [serverSnapshot]);\n\n return useSyncExternalStore(subscribeToResize, readSize, getServerSnapshot);\n}\n"]}
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_MOBILE_MAX_WIDTH = 768;
5
+ var DEFAULT_VIEWPORT_COOKIE = "viewport";
6
+
7
+ // src/script.ts
8
+ function assertSafeToken(value, label) {
9
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
10
+ throw new Error(`Invalid ${label}: use only letters, numbers, _ or -`);
11
+ }
12
+ }
13
+ function parseViewportCookie(value) {
14
+ if (value === "mobile") return true;
15
+ if (value === "desktop") return false;
16
+ return void 0;
17
+ }
18
+ function getViewportCookieScript(options = {}) {
19
+ const cookieName = options.cookieName ?? DEFAULT_VIEWPORT_COOKIE;
20
+ const breakpoint = options.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
21
+ const maxAgeSeconds = options.maxAgeSeconds ?? 60 * 60 * 24 * 7;
22
+ assertSafeToken(cookieName, "cookieName");
23
+ if (!Number.isInteger(breakpoint) || breakpoint <= 0) {
24
+ throw new Error("breakpoint must be a positive integer");
25
+ }
26
+ if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
27
+ throw new Error("maxAgeSeconds must be a positive integer");
28
+ }
29
+ return `(function(){try{var v=window.matchMedia("(max-width: ${breakpoint - 1}px)").matches?"mobile":"desktop";document.cookie="${cookieName}="+v+";path=/;max-age=${maxAgeSeconds};samesite=lax";}catch(e){}})();`;
30
+ }
31
+
32
+ exports.getViewportCookieScript = getViewportCookieScript;
33
+ exports.parseViewportCookie = parseViewportCookie;
34
+ //# sourceMappingURL=script.cjs.map
35
+ //# sourceMappingURL=script.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/script.ts"],"names":[],"mappings":";;;AAAO,IAAM,wBAAA,GAA2B,GAAA;AAEjC,IAAM,uBAAA,GAA0B,UAAA;;;ACSvC,SAAS,eAAA,CAAgB,OAAe,KAAA,EAAqB;AAC3D,EAAA,IAAI,CAAC,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAA,EAAG;AACnC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,mCAAA,CAAqC,CAAA;AAAA,EACvE;AACF;AAEO,SAAS,oBACd,KAAA,EACqB;AACrB,EAAA,IAAI,KAAA,KAAU,UAAU,OAAO,IAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,WAAW,OAAO,KAAA;AAChC,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,uBAAA,CACd,OAAA,GAAuC,EAAC,EAChC;AACR,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,uBAAA;AACzC,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,wBAAA;AACzC,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,aAAA,IAAiB,EAAA,GAAK,KAAK,EAAA,GAAK,CAAA;AAE9D,EAAA,eAAA,CAAgB,YAAY,YAAY,CAAA;AAExC,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,UAAU,CAAA,IAAK,cAAc,CAAA,EAAG;AACpD,IAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,EACzD;AAEA,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,aAAa,CAAA,IAAK,iBAAiB,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAI,MAAM,0CAA0C,CAAA;AAAA,EAC5D;AAEA,EAAA,OAAO,wDAAwD,UAAA,GAAa,CAAC,CAAA,kDAAA,EAAqD,UAAU,yBAAyB,aAAa,CAAA,+BAAA,CAAA;AACpL","file":"script.cjs","sourcesContent":["export const DEFAULT_MOBILE_MAX_WIDTH = 768;\nexport const DEFAULT_TABLET_MAX_WIDTH = 1024;\nexport const DEFAULT_VIEWPORT_COOKIE = \"viewport\";\n","import {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_VIEWPORT_COOKIE,\n} from \"./constants\";\n\nexport interface ViewportCookieScriptOptions {\n cookieName?: string;\n breakpoint?: number;\n maxAgeSeconds?: number;\n}\n\nfunction assertSafeToken(value: string, label: string): void {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: use only letters, numbers, _ or -`);\n }\n}\n\nexport function parseViewportCookie(\n value: string | undefined | null,\n): boolean | undefined {\n if (value === \"mobile\") return true;\n if (value === \"desktop\") return false;\n return undefined;\n}\n\nexport function getViewportCookieScript(\n options: ViewportCookieScriptOptions = {},\n): string {\n const cookieName = options.cookieName ?? DEFAULT_VIEWPORT_COOKIE;\n const breakpoint = options.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const maxAgeSeconds = options.maxAgeSeconds ?? 60 * 60 * 24 * 7;\n\n assertSafeToken(cookieName, \"cookieName\");\n\n if (!Number.isInteger(breakpoint) || breakpoint <= 0) {\n throw new Error(\"breakpoint must be a positive integer\");\n }\n\n if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {\n throw new Error(\"maxAgeSeconds must be a positive integer\");\n }\n\n return `(function(){try{var v=window.matchMedia(\"(max-width: ${breakpoint - 1}px)\").matches?\"mobile\":\"desktop\";document.cookie=\"${cookieName}=\"+v+\";path=/;max-age=${maxAgeSeconds};samesite=lax\";}catch(e){}})();`;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ interface ViewportCookieScriptOptions {
2
+ cookieName?: string;
3
+ breakpoint?: number;
4
+ maxAgeSeconds?: number;
5
+ }
6
+ declare function parseViewportCookie(value: string | undefined | null): boolean | undefined;
7
+ declare function getViewportCookieScript(options?: ViewportCookieScriptOptions): string;
8
+
9
+ export { type ViewportCookieScriptOptions, getViewportCookieScript, parseViewportCookie };
@@ -0,0 +1,9 @@
1
+ interface ViewportCookieScriptOptions {
2
+ cookieName?: string;
3
+ breakpoint?: number;
4
+ maxAgeSeconds?: number;
5
+ }
6
+ declare function parseViewportCookie(value: string | undefined | null): boolean | undefined;
7
+ declare function getViewportCookieScript(options?: ViewportCookieScriptOptions): string;
8
+
9
+ export { type ViewportCookieScriptOptions, getViewportCookieScript, parseViewportCookie };
package/dist/script.js ADDED
@@ -0,0 +1,32 @@
1
+ // src/constants.ts
2
+ var DEFAULT_MOBILE_MAX_WIDTH = 768;
3
+ var DEFAULT_VIEWPORT_COOKIE = "viewport";
4
+
5
+ // src/script.ts
6
+ function assertSafeToken(value, label) {
7
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
8
+ throw new Error(`Invalid ${label}: use only letters, numbers, _ or -`);
9
+ }
10
+ }
11
+ function parseViewportCookie(value) {
12
+ if (value === "mobile") return true;
13
+ if (value === "desktop") return false;
14
+ return void 0;
15
+ }
16
+ function getViewportCookieScript(options = {}) {
17
+ const cookieName = options.cookieName ?? DEFAULT_VIEWPORT_COOKIE;
18
+ const breakpoint = options.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;
19
+ const maxAgeSeconds = options.maxAgeSeconds ?? 60 * 60 * 24 * 7;
20
+ assertSafeToken(cookieName, "cookieName");
21
+ if (!Number.isInteger(breakpoint) || breakpoint <= 0) {
22
+ throw new Error("breakpoint must be a positive integer");
23
+ }
24
+ if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
25
+ throw new Error("maxAgeSeconds must be a positive integer");
26
+ }
27
+ return `(function(){try{var v=window.matchMedia("(max-width: ${breakpoint - 1}px)").matches?"mobile":"desktop";document.cookie="${cookieName}="+v+";path=/;max-age=${maxAgeSeconds};samesite=lax";}catch(e){}})();`;
28
+ }
29
+
30
+ export { getViewportCookieScript, parseViewportCookie };
31
+ //# sourceMappingURL=script.js.map
32
+ //# sourceMappingURL=script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/script.ts"],"names":[],"mappings":";AAAO,IAAM,wBAAA,GAA2B,GAAA;AAEjC,IAAM,uBAAA,GAA0B,UAAA;;;ACSvC,SAAS,eAAA,CAAgB,OAAe,KAAA,EAAqB;AAC3D,EAAA,IAAI,CAAC,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAA,EAAG;AACnC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,mCAAA,CAAqC,CAAA;AAAA,EACvE;AACF;AAEO,SAAS,oBACd,KAAA,EACqB;AACrB,EAAA,IAAI,KAAA,KAAU,UAAU,OAAO,IAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,WAAW,OAAO,KAAA;AAChC,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,uBAAA,CACd,OAAA,GAAuC,EAAC,EAChC;AACR,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,uBAAA;AACzC,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,wBAAA;AACzC,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,aAAA,IAAiB,EAAA,GAAK,KAAK,EAAA,GAAK,CAAA;AAE9D,EAAA,eAAA,CAAgB,YAAY,YAAY,CAAA;AAExC,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,UAAU,CAAA,IAAK,cAAc,CAAA,EAAG;AACpD,IAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,EACzD;AAEA,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,aAAa,CAAA,IAAK,iBAAiB,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAI,MAAM,0CAA0C,CAAA;AAAA,EAC5D;AAEA,EAAA,OAAO,wDAAwD,UAAA,GAAa,CAAC,CAAA,kDAAA,EAAqD,UAAU,yBAAyB,aAAa,CAAA,+BAAA,CAAA;AACpL","file":"script.js","sourcesContent":["export const DEFAULT_MOBILE_MAX_WIDTH = 768;\nexport const DEFAULT_TABLET_MAX_WIDTH = 1024;\nexport const DEFAULT_VIEWPORT_COOKIE = \"viewport\";\n","import {\n DEFAULT_MOBILE_MAX_WIDTH,\n DEFAULT_VIEWPORT_COOKIE,\n} from \"./constants\";\n\nexport interface ViewportCookieScriptOptions {\n cookieName?: string;\n breakpoint?: number;\n maxAgeSeconds?: number;\n}\n\nfunction assertSafeToken(value: string, label: string): void {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: use only letters, numbers, _ or -`);\n }\n}\n\nexport function parseViewportCookie(\n value: string | undefined | null,\n): boolean | undefined {\n if (value === \"mobile\") return true;\n if (value === \"desktop\") return false;\n return undefined;\n}\n\nexport function getViewportCookieScript(\n options: ViewportCookieScriptOptions = {},\n): string {\n const cookieName = options.cookieName ?? DEFAULT_VIEWPORT_COOKIE;\n const breakpoint = options.breakpoint ?? DEFAULT_MOBILE_MAX_WIDTH;\n const maxAgeSeconds = options.maxAgeSeconds ?? 60 * 60 * 24 * 7;\n\n assertSafeToken(cookieName, \"cookieName\");\n\n if (!Number.isInteger(breakpoint) || breakpoint <= 0) {\n throw new Error(\"breakpoint must be a positive integer\");\n }\n\n if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {\n throw new Error(\"maxAgeSeconds must be a positive integer\");\n }\n\n return `(function(){try{var v=window.matchMedia(\"(max-width: ${breakpoint - 1}px)\").matches?\"mobile\":\"desktop\";document.cookie=\"${cookieName}=\"+v+\";path=/;max-age=${maxAgeSeconds};samesite=lax\";}catch(e){}})();`;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "react-mobile-viewport",
3
+ "version": "0.1.0",
4
+ "description": "SSR-safe React hooks to detect mobile, tablet, and desktop viewports. Works with Next.js App Router.",
5
+ "license": "MIT",
6
+ "author": "Narges Abolhasan",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nargesabolhasan/react-mobile-viewport.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/nargesabolhasan/react-mobile-viewport/issues"
13
+ },
14
+ "homepage": "https://github.com/nargesabolhasan/react-mobile-viewport#readme",
15
+ "keywords": [
16
+ "react",
17
+ "nextjs",
18
+ "next",
19
+ "viewport",
20
+ "mobile",
21
+ "media-query",
22
+ "matchMedia",
23
+ "ssr",
24
+ "hooks",
25
+ "responsive"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./script": {
44
+ "import": {
45
+ "types": "./dist/script.d.ts",
46
+ "default": "./dist/script.js"
47
+ },
48
+ "require": {
49
+ "types": "./dist/script.d.cts",
50
+ "default": "./dist/script.cjs"
51
+ }
52
+ }
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "README.md",
57
+ "LICENSE"
58
+ ],
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
68
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
69
+ },
70
+ "peerDependencies": {
71
+ "react": ">=18",
72
+ "react-dom": ">=18"
73
+ },
74
+ "devDependencies": {
75
+ "@testing-library/react": "^16.3.0",
76
+ "@types/node": "^22.20.3",
77
+ "@types/react": "^19.1.8",
78
+ "@types/react-dom": "^19.1.6",
79
+ "jsdom": "^26.1.0",
80
+ "react": "^19.1.0",
81
+ "react-dom": "^19.1.0",
82
+ "tsup": "^8.5.0",
83
+ "typescript": "^5.8.3",
84
+ "vitest": "^3.2.4"
85
+ }
86
+ }