├── .gitignore ├── example └── next-app │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── pages │ ├── api │ │ └── hello.js │ ├── _app.js │ └── index.js │ ├── styles │ ├── globals.css │ └── Home.module.css │ ├── package.json │ ├── .gitignore │ ├── README.md │ └── yarn.lock ├── .babelrc ├── src ├── utils.ts ├── components.tsx ├── hooks.ts └── index.tsx ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .next 5 | -------------------------------------------------------------------------------- /example/next-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/next-real-viewport/HEAD/example/next-app/public/favicon.ico -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-proposal-optional-chaining", 5 | { 6 | "loose": true 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /example/next-app/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /example/next-app/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { RealViewportProvider } from "next-real-viewport"; 2 | import "../styles/globals.css"; 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | export default MyApp; 13 | -------------------------------------------------------------------------------- /example/next-app/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /example/next-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "10.0.7", 12 | "react": "17.0.1", 13 | "react-dom": "17.0.1", 14 | "next-real-viewport": "../../dist" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | function debounce(func: () => void, wait: number, immediate?: boolean) { 2 | let timeout: number | null; 3 | return function () { 4 | timeout && window.clearTimeout(timeout); 5 | timeout = window.setTimeout(() => { 6 | timeout = null; 7 | if (!immediate) func(); 8 | }, wait); 9 | if (immediate && !timeout) func(); 10 | }; 11 | } 12 | 13 | export { debounce }; 14 | -------------------------------------------------------------------------------- /example/next-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "allowJs": true, 5 | "jsx": "preserve", 6 | "target": "esnext", 7 | "module": "esnext", 8 | "lib": ["dom", "es2019"], 9 | "noEmit": true, 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictBindCallApply": true, 17 | "strictPropertyInitialization": true, 18 | "noImplicitThis": true, 19 | "alwaysStrict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "skipLibCheck": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "esModuleInterop": true, 26 | "resolveJsonModule": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-real-viewport", 3 | "repository": "git://github.com/basementstudio/next-real-viewport.git", 4 | "author": "Julián Benegas ", 5 | "version": "0.7.2", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.modern.js", 8 | "types": "./dist/index.d.ts", 9 | "source": "./src/index.tsx", 10 | "license": "MIT", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "prepublish": "yarn build", 16 | "build": "microbundle --jsx React.createElement --compress --no-sourcemap" 17 | }, 18 | "dependencies": {}, 19 | "peerDependencies": { 20 | "next": "^12.1.0", 21 | "react": "*", 22 | "react-dom": "*" 23 | }, 24 | "devDependencies": { 25 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 26 | "@types/react": "^16.9.53", 27 | "microbundle": "^0.12.3", 28 | "next": "^12.1.0", 29 | "typescript": "^4.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | 3 | type WithCenterProps = JSX.IntrinsicElements["div"] & { 4 | center?: boolean; 5 | useNativeValues?: boolean; 6 | }; 7 | 8 | const ViewportWidthBox = forwardRef( 9 | ({ style, center = true, useNativeValues, ...rest }, ref) => ( 10 |
21 | ) 22 | ); 23 | 24 | const ViewportHeightBox = forwardRef< 25 | HTMLDivElement, 26 | JSX.IntrinsicElements["div"] 27 | >(({ style, ...rest }, ref) => ( 28 |
33 | )); 34 | 35 | export { ViewportWidthBox, ViewportHeightBox }; 36 | -------------------------------------------------------------------------------- /example/next-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useIOSToolbarState = () => { 4 | const [isVisible, setIsVisible] = useState(); 5 | 6 | useEffect(() => { 7 | const ua = window.navigator ? window.navigator.userAgent : ""; 8 | const iOS = ua.match(/iPad/i) || ua.match(/iPhone/i); 9 | const webkit = ua.match(/WebKit/i); 10 | const iOSSafari = iOS && webkit && !ua.match(/CriOS/i); 11 | const baseWindowHeight = window.innerHeight; 12 | 13 | function handleScroll() { 14 | const newWindowHeight = window.innerHeight; 15 | if (newWindowHeight - 50 > baseWindowHeight) { 16 | setIsVisible(false); 17 | } else { 18 | setIsVisible(true); 19 | } 20 | } 21 | 22 | // the toolbar issue only happens on iOS Safari 23 | if (iOSSafari) { 24 | if ( 25 | "standalone" in window.navigator && 26 | (window.navigator as any)["standalone"] 27 | ) { 28 | // if it's iOS' standalone mode (added to home screen) 29 | // the toolbar is always "hidden" 30 | setIsVisible(false); 31 | } else { 32 | // iOS Safari 33 | document.addEventListener("scroll", handleScroll); 34 | 35 | return () => { 36 | document.removeEventListener("scroll", handleScroll); 37 | }; 38 | } 39 | } 40 | }, []); 41 | 42 | return { isVisible }; 43 | }; 44 | -------------------------------------------------------------------------------- /example/next-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /example/next-app/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 4rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 1rem; 85 | flex-basis: 45%; 86 | padding: 1.5rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | .card:hover, 96 | .card:focus, 97 | .card:active { 98 | color: #0070f3; 99 | border-color: #0070f3; 100 | } 101 | 102 | .card h3 { 103 | margin: 0 0 1rem 0; 104 | font-size: 1.5rem; 105 | } 106 | 107 | .card p { 108 | margin: 0; 109 | font-size: 1.25rem; 110 | line-height: 1.5; 111 | } 112 | 113 | .logo { 114 | height: 1em; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/next-app/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { ViewportWidthBox } from "next-real-viewport"; 3 | import styles from "../styles/Home.module.css"; 4 | 5 | export default function Home() { 6 | return ( 7 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | import React, { 3 | createContext, 4 | memo, 5 | useContext, 6 | useEffect, 7 | useState, 8 | } from "react"; 9 | import { useCallback } from "react"; 10 | import { useIOSToolbarState } from "./hooks"; 11 | import { debounce } from "./utils"; 12 | 13 | const vwCssVar = "vw"; 14 | const vhCssVar = "vh"; 15 | 16 | interface Context { 17 | vw: number | undefined; 18 | vh: number | undefined; 19 | isIOSToolbarVisible: boolean | undefined; 20 | } 21 | 22 | const RealViewportContext = createContext(undefined); 23 | 24 | const encodeBase64 = (str: string) => { 25 | return typeof window !== "undefined" 26 | ? window.btoa(str) 27 | : Buffer.from(str).toString("base64"); 28 | }; 29 | 30 | const RealViewportScript = memo(({ prefix }: { prefix: string }) => { 31 | const encodedScript = `data:text/javascript;base64,${encodeBase64(`(function() { 32 | var d = document.documentElement; 33 | d.style.setProperty('--${ 34 | prefix + vwCssVar 35 | }', (d.clientWidth || window.innerWidth) / 100 + 'px'); 36 | d.style.setProperty('--${ 37 | prefix + vhCssVar 38 | }', (d.clientHeight || window.innerHeight) / 100 + 'px'); 39 | }())`)}`; 40 | return ( 41 |