├── .eslintrc.js ├── .gitattributes ├── .github ├── actions │ └── build │ │ └── action.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── pages.yml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── packages ├── react-device-frameset-example │ ├── .gitignore │ ├── declaration.d.ts │ ├── index.html │ ├── package.json │ ├── pages │ │ ├── $.mdx │ │ ├── _theme.tsx │ │ ├── device$.mdx │ │ ├── emulator$.mdx │ │ ├── legal$.mdx │ │ └── zommable$.tsx │ └── vite.config.ts └── react-device-frameset │ ├── README.md │ ├── gulpfile.js │ ├── package.json │ ├── scss │ ├── device-emulator.scss │ ├── device-selector.scss │ └── marvel-devices.scss │ └── src │ ├── DeviceEmulator.tsx │ ├── DeviceFrameset.tsx │ ├── DeviceOptions.ts │ ├── DeviceSelector.tsx │ ├── Zoomable.tsx │ ├── helper.ts │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaFeatures: { 6 | jsx: true, 7 | }, 8 | ecmaVersion: 'latest', 9 | sourceType: 'module', 10 | tsconfigRootDir: __dirname, 11 | project: ['./tsconfig.json'], 12 | }, 13 | env: { 14 | browser: true, 15 | node: true, 16 | }, 17 | ignorePatterns: ['**/{node_modules,dist}', 'plugins', '*.js'], 18 | extends: [ 19 | 'eslint:recommended', 20 | 'plugin:react/recommended', 21 | 'plugin:react/jsx-runtime', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:promise/recommended', 24 | 'plugin:react-hooks/recommended', 25 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 26 | ], 27 | plugins: ['react', '@typescript-eslint'], 28 | settings: { 29 | react: { 30 | createClass: 'createReactClass', 31 | pragma: 'React', 32 | fragment: 'Fragment', 33 | version: 'detect', 34 | }, 35 | }, 36 | rules: { 37 | quotes: ['error', 'single'], 38 | 'react/prop-types': 'off', 39 | '@typescript-eslint/no-unused-vars': [ 40 | 'error', 41 | { 42 | varsIgnorePattern: '^_', 43 | }, 44 | ], 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.scss linguist-detectable=false 2 | *.css linguist-detectable=false 3 | 4 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build PNPM Project 2 | description: Install PNPM dependencies and execute build scripts 3 | inputs: 4 | node-version: 5 | default: '16.x' 6 | runs: 7 | using: composite 8 | steps: 9 | - name: Setup Node 10 | uses: actions/setup-node@v2 11 | with: 12 | node-version: ${{ inputs.node-version }} 13 | 14 | - name: Cache PNPM modules 15 | uses: actions/cache@v2 16 | with: 17 | path: ~/.pnpm-store 18 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 19 | restore-keys: | 20 | ${{ runner.os }}- 21 | 22 | - name: Setup PNPM 23 | uses: pnpm/action-setup@v2.1.0 24 | with: 25 | version: 6 26 | run_install: true 27 | 28 | - name: Lint 29 | run: pnpm lint 30 | shell: bash 31 | 32 | - name: Build 33 | run: pnpm build 34 | shell: bash 35 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - workflow_call 7 | 8 | jobs: 9 | install-build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Build 17 | uses: ./.github/actions/build -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '17 21 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | pages: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Build 16 | uses: ./.github/actions/build 17 | 18 | - name: Deploy 19 | uses: crazy-max/ghaction-github-pages@v2 20 | with: 21 | target_branch: gh-pages 22 | build_dir: packages/react-device-frameset-example/dist 23 | fqdn: react-device-frameset.zheeeng.me 24 | author: Zheeeng 25 | jekyll: false 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | if: github.repository == 'zheeeng/react-device-frameset' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Build 17 | uses: ./.github/actions/build 18 | 19 | - name: Publish 20 | uses: JS-DevTools/npm-publish@v1 21 | with: 22 | token: ${{ secrets.NPM_TOKEN }} 23 | package: ./packages/react-device-frameset/package.json 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | lib 5 | *.local 6 | *.tgz 7 | .yarn-error.log -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zheeeng 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Device Frameset 2 | 3 | [![NPM](https://nodei.co/npm/react-device-frameset.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-device-frameset/) 4 | 5 | ![publish workflow](https://github.com/zheeeng/react-device-frameset/actions/workflows/publish.yml/badge.svg) 6 | ![pages workflow](https://github.com/zheeeng/react-device-frameset/actions/workflows/pages.yml/badge.svg) 7 | [![npm version](https://img.shields.io/npm/v/react-device-frameset.svg)](https://www.npmjs.com/package/react-device-frameset) 8 | 9 | This is yet another device frameset component for React. 10 | 11 | ## [Demo](https://react-device-frameset.zheeeng.me) 12 | 13 | ## Features 14 | 15 | * Powered by pure css device prototype showcase [Marvel Devices.css](http://marvelapp.github.io/devices.css/) 16 | * [![language](https://img.shields.io/badge/%3C%2F%3E-TypeScript-blue.svg)](http://typescriptlang.org/) Type Safe and under maintainable 17 | * Sample for reference 18 | 19 | * Device Selector 20 | ![frameset-screenshot](https://user-images.githubusercontent.com/1303154/120062053-a58a6200-c092-11eb-9fec-fa0dd3609645.png) 21 | 22 | * Device Emulator 23 | ![frameset-screenshot](https://user-images.githubusercontent.com/1303154/132490604-f6d05da8-835d-437f-9b10-5ffec76e661f.png) 24 | 25 | ## Installation 26 | 27 | ```bash 28 | yarn add react-device-frameset (or npm/pnpm) 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Stylesheet importing 34 | 35 | `react-device-frameset` supports [conditional exports](https://nodejs.org/api/packages.html#conditional-exports). 36 | 37 | If the application bundler supports this feature and above node v12.11.0, you can import the stylesheet through the recommended path `react-device-frameset/styles`, it is largely supported in real developing environments, otherwise, you need to import it from `react-device-frameset/dist/styles`. 38 | 39 | ### Basic Example 40 | 41 | ```tsx 42 | import { DeviceFrameset } from 'react-device-frameset' 43 | import 'react-device-frameset/styles/marvel-devices.min.css' 44 | 45 | export const App = () => { 46 | return ( 47 | 48 |
Hello world
49 |
50 | ) 51 | } 52 | ``` 53 | 54 | ### Props Signature 55 | 56 | DeviceFramesetProps: 57 | 58 | ```ts (signature) 59 | | { device: 'iPhone X', landscape?: boolean, width?: number, height?: number, zoom?: number } 60 | | { device: 'iPhone 8', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 61 | | { device: 'iPhone 8 Plus', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 62 | | { device: 'iPhone 5s', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 63 | | { device: 'iPhone 5c', color: 'white' | 'red' | 'yellow' | 'green' | 'blue', landscape?: boolean, width?: number, height?: number, zoom?: number } 64 | | { device: 'iPhone 4s', color: 'black' | 'silver', landscape?: boolean, width?: number, height?: number, zoom?: number } 65 | | { device: 'Galaxy Note 8', landscape?: boolean, width?: number, height?: number, zoom?: number } 66 | | { device: 'Nexus 5', landscape?: boolean, width?: number, height?: number, zoom?: number } 67 | | { device: 'Lumia 920', color: 'black' | 'white' | 'yellow' | 'red' | 'blue', landscape?: boolean, width?: number, height?: number, zoom?: number } 68 | | { device: 'Samsung Galaxy S5', color: 'white' | 'black', landscape?: boolean, width?: number, height?: number, zoom?: number } 69 | | { device: 'HTC One', landscape?: boolean, width?: number, height?: number, zoom?: number } 70 | | { device: 'iPad Mini', color: 'black' | 'silver', landscape?: boolean, width?: number, height?: number, zoom?: number } 71 | | { device: 'MacBook Pro', width?: number, height?: number, zoom?: number } 72 | ``` 73 | 74 | ## If you like the frameset selector? 75 | 76 | ```ts (signature) 77 | type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPhone 5c" | "iPhone 4s" | "Galaxy Note 8" | "Nexus 5" | "Lumia 920" | "Samsung Galaxy S5" | "HTC One" | "iPad Mini" | "MacBook Pro" 78 | 79 | type DeviceEmulatorProps = { 80 | banDevices?: DeviceName[] 81 | children: (props: DeviceFramesetProps) => React.ReactNode, 82 | value?: DeviceName, 83 | onChange?: (deviceName: DeviceName) => void, 84 | } 85 | ``` 86 | 87 | ```tsx 88 | import { DeviceFrameset, DeviceSelector } from 'react-device-frameset' 89 | import 'react-device-frameset/styles/marvel-devices.min.css' 90 | import 'react-device-frameset/styles/device-selector.min.css' 91 | 92 | export const App = () => { 93 | return ( 94 | 95 | {props => } 96 | 97 | ) 98 | } 99 | ``` 100 | 101 | ## If you like the frameset emulator? 102 | 103 | ```ts (signature) 104 | type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPhone 5c" | "iPhone 4s" | "Galaxy Note 8" | "Nexus 5" | "Lumia 920" | "Samsung Galaxy S5" | "HTC One" | "iPad Mini" | "MacBook Pro" 105 | 106 | type DeviceEmulatorProps = { 107 | banDevices?: DeviceName[] 108 | children: (props: DeviceFramesetProps) => React.ReactNode, 109 | value?: DeviceFramesetProps, 110 | onChange?: (deviceConfig: DeviceFramesetProps) => void, 111 | } 112 | ``` 113 | 114 | ```tsx 115 | import { DeviceFrameset, DeviceEmulator } from 'react-device-frameset' 116 | import 'react-device-frameset/styles/marvel-devices.min.css' 117 | import 'react-device-frameset/styles/device-emulator.min.css' 118 | 119 | export const App = () => { 120 | return ( 121 | 122 | {props => } 123 | 124 | ) 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Zheeeng ", 3 | "private": false, 4 | "scripts": { 5 | "dev": "npm-run-all --parallel dev:*", 6 | "dev:lib": "pnpm --filter react-device-frameset dev", 7 | "dev:example": "pnpm --filter react-device-frameset-example dev", 8 | "build": "pnpm build:lib && pnpm build:example", 9 | "build:lib": "pnpm --filter react-device-frameset build", 10 | "build:example": "pnpm --filter react-device-frameset-example build", 11 | "lint": "eslint ./**/*.{ts,tsx}", 12 | "lint:fix": "eslint ./**/*.{ts,tsx} --fix" 13 | }, 14 | "dependencies": { 15 | "react": "^18.2.0", 16 | "react-device-frameset": "workspace:*", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.14.2" 19 | }, 20 | "devDependencies": { 21 | "@mdx-js/react": "^2.1.5", 22 | "@types/node": "^16.11.7", 23 | "@typescript-eslint/eslint-plugin": "^5.62.0", 24 | "@typescript-eslint/parser": "^5.62.0", 25 | "eslint": "^8.45.0", 26 | "eslint-plugin-promise": "^6.1.1", 27 | "eslint-plugin-react": "^7.33.0", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "npm-run-all": "^4.1.5", 30 | "pnpm": "^8.6.10", 31 | "typescript": "^5.1.6" 32 | }, 33 | "engines": { 34 | "node": ">=16" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local -------------------------------------------------------------------------------- /packages/react-device-frameset-example/declaration.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /packages/react-device-frameset-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Device Frameset 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-device-frameset-example", 3 | "private": true, 4 | "description": "react-device-frameset example", 5 | "scripts": { 6 | "dev": "vite serve", 7 | "build": "pnpm build:site", 8 | "build:site": "rm -rf dist && vite build --outDir dist", 9 | "serve": "serve -s dist" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^17.0.3", 13 | "@types/react-router-dom": "^5.1.7", 14 | "@vitejs/plugin-react": "^4.0.3", 15 | "serve": "^14.2.0", 16 | "vite": "^4.5.2", 17 | "vite-pages-theme-doc": "^4.1.6", 18 | "vite-plugin-react-pages": "^4.1.4", 19 | "vite-plugin-virtual-plain-text": "^1.4.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: README 3 | order: 0 4 | --- 5 | 6 | import README from '../../../README.md' 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/_theme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from 'vite-pages-theme-doc' 2 | 3 | export default createTheme({ 4 | topNavs: [ 5 | { 6 | label: 'Github ⭐', 7 | href: 'https://github.com/zheeeng/react-device-frameset', 8 | }, 9 | ], 10 | logo: 'React Device Frameset', 11 | }) 12 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/device$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: DeviceFrameset 3 | order: 1 4 | --- 5 | 6 | import { DeviceFrameset, DeviceSelector } from 'react-device-frameset' 7 | import 'react-device-frameset/styles/device-selector.css' 8 | import 'react-device-frameset/styles/marvel-devices.css' 9 | 10 | 11 | {props => } 12 | 13 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/emulator$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: DeviceEmulator 3 | order: 2 4 | --- 5 | 6 | import { DeviceFrameset, DeviceEmulator } from 'react-device-frameset' 7 | import 'react-device-frameset/styles/device-emulator.css' 8 | import 'react-device-frameset/styles/marvel-devices.css' 9 | 10 | 11 | {props => } 12 | 13 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/legal$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: LICENSE 3 | order: 1000 4 | --- 5 | 6 | import { plainText as LICENSE } from '@virtual:plain-text/../../LICENSE' 7 | 8 |
9 | {LICENSE} 10 |
-------------------------------------------------------------------------------- /packages/react-device-frameset-example/pages/zommable$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Zoomable 3 | * @order 3 4 | */ 5 | 6 | import { Zoomable } from 'react-device-frameset' 7 | 8 | const Demo = () => ( 9 |
10 | 11 |
21 | width: 300, height: 400 22 |
23 |
24 | 25 |
35 | width: 305, height: 405 36 |
37 |
38 | 39 |
49 | width: 150, height: 200 50 |
51 |
52 | 53 |
63 | width: 600, height: 800 64 |
65 |
66 | 67 |
77 | width: 200, height: 800 78 |
79 |
80 | 81 |
91 | width: 300, height: 800 92 |
93 |
94 | 95 |
105 | width: 200, height: 200 106 |
107 |
108 | 109 |
119 | width: 400, height: 800 120 |
121 |
122 | 123 |
133 | width: 300, height: 200 134 |
135 |
136 | 137 |
147 | width: 400, height: 200 148 |
149 |
150 | 151 |
161 | width: 600, height: 800 162 |
163 |
164 | 165 |
175 | width: 605, height: 805 176 |
177 |
178 | 179 |
189 | width: 595, height: 795 190 |
191 |
192 |
193 | ) 194 | 195 | export default Demo 196 | -------------------------------------------------------------------------------- /packages/react-device-frameset-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import * as path from 'path' 3 | import react from '@vitejs/plugin-react' 4 | import pages from 'vite-plugin-react-pages' 5 | import virtualPlainText from 'vite-plugin-virtual-plain-text' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | pages({ 11 | pagesDir: path.join(__dirname, 'pages'), 12 | }), 13 | virtualPlainText({ 14 | namedExport: 'plainText' 15 | }) 16 | ], 17 | }) 18 | -------------------------------------------------------------------------------- /packages/react-device-frameset/README.md: -------------------------------------------------------------------------------- 1 | # React Device Frameset 2 | 3 | [![NPM](https://nodei.co/npm/react-device-frameset.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-device-frameset/) 4 | 5 | ![publish workflow](https://github.com/zheeeng/react-device-frameset/actions/workflows/publish.yml/badge.svg) 6 | ![pages workflow](https://github.com/zheeeng/react-device-frameset/actions/workflows/pages.yml/badge.svg) 7 | [![npm version](https://img.shields.io/npm/v/react-device-frameset.svg)](https://www.npmjs.com/package/react-device-frameset) 8 | 9 | This is yet another device frameset component for React. 10 | 11 | ## [Demo](https://react-device-frameset.zheeeng.me) 12 | 13 | ## Features 14 | 15 | * Powered by pure css device prototype showcase [Marvel Devices.css](http://marvelapp.github.io/devices.css/) 16 | * [![language](https://img.shields.io/badge/%3C%2F%3E-TypeScript-blue.svg)](http://typescriptlang.org/) Type Safe and under maintainable 17 | * Sample for reference 18 | 19 | * Device Selector 20 | ![frameset-screenshot](https://user-images.githubusercontent.com/1303154/120062053-a58a6200-c092-11eb-9fec-fa0dd3609645.png) 21 | 22 | * Device Emulator 23 | ![frameset-screenshot](https://user-images.githubusercontent.com/1303154/132490604-f6d05da8-835d-437f-9b10-5ffec76e661f.png) 24 | 25 | ## Installation 26 | 27 | ```bash 28 | yarn add react-device-frameset (or npm/pnpm) 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Stylesheet importing 34 | 35 | `react-device-frameset` supports [conditional exports](https://nodejs.org/api/packages.html#conditional-exports). 36 | 37 | If the application bundler supports this feature and above node v12.11.0, you can import the stylesheet through the recommended path `react-device-frameset/styles`, it is largely supported in real developing environments, otherwise, you need to import it from `react-device-frameset/dist/styles`. 38 | 39 | ### Basic Example 40 | 41 | ```tsx 42 | import { DeviceFrameset } from 'react-device-frameset' 43 | import 'react-device-frameset/styles/marvel-devices.min.css' 44 | 45 | export const App = () => { 46 | return ( 47 | 48 |
Hello world
49 |
50 | ) 51 | } 52 | ``` 53 | 54 | ### Props Signature 55 | 56 | DeviceFramesetProps: 57 | 58 | ```ts (signature) 59 | | { device: 'iPhone X', landscape?: boolean, width?: number, height?: number, zoom?: number } 60 | | { device: 'iPhone 8', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 61 | | { device: 'iPhone 8 Plus', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 62 | | { device: 'iPhone 5s', color: 'black' | 'silver' | 'gold', landscape?: boolean, width?: number, height?: number, zoom?: number } 63 | | { device: 'iPhone 5c', color: 'white' | 'red' | 'yellow' | 'green' | 'blue', landscape?: boolean, width?: number, height?: number, zoom?: number } 64 | | { device: 'iPhone 4s', color: 'black' | 'silver', landscape?: boolean, width?: number, height?: number, zoom?: number } 65 | | { device: 'Galaxy Note 8', landscape?: boolean, width?: number, height?: number, zoom?: number } 66 | | { device: 'Nexus 5', landscape?: boolean, width?: number, height?: number, zoom?: number } 67 | | { device: 'Lumia 920', color: 'black' | 'white' | 'yellow' | 'red' | 'blue', landscape?: boolean, width?: number, height?: number, zoom?: number } 68 | | { device: 'Samsung Galaxy S5', color: 'white' | 'black', landscape?: boolean, width?: number, height?: number, zoom?: number } 69 | | { device: 'HTC One', landscape?: boolean, width?: number, height?: number, zoom?: number } 70 | | { device: 'iPad Mini', color: 'black' | 'silver', landscape?: boolean, width?: number, height?: number, zoom?: number } 71 | | { device: 'MacBook Pro', width?: number, height?: number, zoom?: number } 72 | ``` 73 | 74 | ## If you like the frameset selector? 75 | 76 | ```ts (signature) 77 | type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPhone 5c" | "iPhone 4s" | "Galaxy Note 8" | "Nexus 5" | "Lumia 920" | "Samsung Galaxy S5" | "HTC One" | "iPad Mini" | "MacBook Pro" 78 | 79 | type DeviceEmulatorProps = { 80 | banDevices?: DeviceName[] 81 | children: (props: DeviceFramesetProps) => React.ReactNode, 82 | value?: DeviceName, 83 | onChange?: (deviceName: DeviceName) => void, 84 | } 85 | ``` 86 | 87 | ```tsx 88 | import { DeviceFrameset, DeviceSelector } from 'react-device-frameset' 89 | import 'react-device-frameset/styles/marvel-devices.min.css' 90 | import 'react-device-frameset/styles/device-selector.min.css' 91 | 92 | export const App = () => { 93 | return ( 94 | 95 | {props => } 96 | 97 | ) 98 | } 99 | ``` 100 | 101 | ## If you like the frameset emulator? 102 | 103 | ```ts (signature) 104 | type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPhone 5c" | "iPhone 4s" | "Galaxy Note 8" | "Nexus 5" | "Lumia 920" | "Samsung Galaxy S5" | "HTC One" | "iPad Mini" | "MacBook Pro" 105 | 106 | type DeviceEmulatorProps = { 107 | banDevices?: DeviceName[] 108 | children: (props: DeviceFramesetProps) => React.ReactNode, 109 | value?: DeviceFramesetProps, 110 | onChange?: (deviceConfig: DeviceFramesetProps) => void, 111 | } 112 | ``` 113 | 114 | ```tsx 115 | import { DeviceFrameset, DeviceEmulator } from 'react-device-frameset' 116 | import 'react-device-frameset/styles/marvel-devices.min.css' 117 | import 'react-device-frameset/styles/device-emulator.min.css' 118 | 119 | export const App = () => { 120 | return ( 121 | 122 | {props => } 123 | 124 | ) 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /packages/react-device-frameset/gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest, watch } = require('gulp'); 2 | 3 | const sass = require('gulp-sass')(require('sass')) 4 | const autoprefixer = require('gulp-autoprefixer') 5 | const rename = require('gulp-rename') 6 | const sourcemaps = require('gulp-sourcemaps'); 7 | const replace = require('gulp-replace'); 8 | 9 | const config = { 10 | srcCss : 'scss/**/*.scss', 11 | buildCss: 'dist/styles' 12 | } 13 | 14 | function buildCss () { 15 | return src(config.srcCss) 16 | .pipe(sourcemaps.init()) 17 | .pipe(sass({ 18 | outputStyle : 'expanded' 19 | }).on('error', sass.logError)) 20 | .pipe(autoprefixer()) 21 | .pipe(dest(config.buildCss)) 22 | .pipe(sass({ 23 | outputStyle : 'compressed' 24 | })) 25 | .pipe(rename({ extname: '.min.css' })) 26 | .pipe(sourcemaps.write('.')) 27 | .pipe(dest(config.buildCss)) 28 | } 29 | 30 | function watchBuildCss () { 31 | function build () { 32 | return src(config.srcCss) 33 | .pipe(sourcemaps.init()) 34 | .pipe(sass({ 35 | outputStyle : 'expanded' 36 | }).on('error', sass.logError)) 37 | .pipe(dest(config.buildCss)) 38 | .pipe(sourcemaps.write('.')) 39 | .pipe(dest(config.buildCss)) 40 | } 41 | 42 | watch(config.srcCss, { ignoreInitial: false }, build) 43 | } 44 | 45 | function genSignature () { 46 | const DeviceOptions = require('./dist/index').DeviceOptions 47 | const signature = Object.keys(DeviceOptions).map((key) => { 48 | const [device, info] = [key, DeviceOptions[key]] 49 | const deviceSignature = `device: '${device}'` 50 | const colorSignature = (info.colors && info.colors.length) ? `, color: '${info.colors.join('\' | \'')}'` : '' 51 | const landscapeSignature = info.hasLandscape ? `, landscape?: boolean` : '' 52 | const widthSignature = `, width?: number` 53 | const heightSignature = `, height?: number` 54 | const zoomSignature = `, zoom?: number` 55 | 56 | return `| { ${deviceSignature}${colorSignature}${landscapeSignature}${widthSignature}${heightSignature}${zoomSignature} }` 57 | }) 58 | .join('\n') 59 | 60 | console.log({ signature }) 61 | 62 | return src('../../README.md') 63 | .pipe(replace(/```ts\s\(signature\)(.|[\r\n])*?```/, `\`\`\`ts (signature)\n${signature}\n\`\`\``)) 64 | .pipe(dest('../../')); 65 | } 66 | 67 | function moveReadme () { 68 | return src('../../README.md') 69 | .pipe(dest('./')); 70 | } 71 | 72 | exports.buildCss = buildCss 73 | exports.watchBuildCss = watchBuildCss 74 | exports.genSignature = genSignature 75 | exports.moveReadme = moveReadme -------------------------------------------------------------------------------- /packages/react-device-frameset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-device-frameset", 3 | "version": "1.3.4", 4 | "author": "Zheeeng ", 5 | "description": "This is yet another device frameset component for React.", 6 | "keywords": [ 7 | "frameset", 8 | "component", 9 | "simulator", 10 | "emulator", 11 | "frame", 12 | "device", 13 | "phone", 14 | "tablet", 15 | "pc", 16 | "mac", 17 | "ios", 18 | "android", 19 | "frameset", 20 | "react", 21 | "component", 22 | "vite", 23 | "typescript" 24 | ], 25 | "repository": "zheeeng/react-device-frameset", 26 | "license": "MIT", 27 | "sideEffects": [ 28 | "dist/styles/*.css" 29 | ], 30 | "main": "dist/index.js", 31 | "module": "dist/index.mjs", 32 | "types": "dist/index.d.ts", 33 | "exports": { 34 | ".": { 35 | "require": "./dist/index.js", 36 | "types": "./dist/index.d.ts", 37 | "default": "./dist/index.mjs" 38 | }, 39 | "./package.json": "./package.json", 40 | "./*": "./dist/*" 41 | }, 42 | "files": [ 43 | "dist" 44 | ], 45 | "scripts": { 46 | "dev": "npm-run-all --parallel dev:*", 47 | "dev:lib": "pnpm build:lib -- --watch", 48 | "dev:css": "gulp watchBuildCss", 49 | "build": "pnpm build:pack", 50 | "build:lib": "tsup src/index.ts --format cjs,esm --dts --clean", 51 | "build:css": "gulp buildCss", 52 | "build:readme": "gulp genSignature && gulp moveReadme", 53 | "build:pack": "pnpm build:lib && pnpm build:css && pnpm build:readme" 54 | }, 55 | "devDependencies": { 56 | "@types/react": "^17.0.39", 57 | "gulp": "^4.0.2", 58 | "gulp-autoprefixer": "^8.0.0", 59 | "gulp-rename": "^2.0.0", 60 | "gulp-replace": "^1.1.3", 61 | "gulp-sass": "^5.1.0", 62 | "gulp-sourcemaps": "^3.0.0", 63 | "sass": "1.64.1", 64 | "tsup": "^7.1.0" 65 | }, 66 | "peerDependencies": { 67 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/react-device-frameset/scss/device-emulator.scss: -------------------------------------------------------------------------------- 1 | .device-emulator { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | 8 | section { 9 | flex-shrink: 0; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | height: 32px; 14 | border-bottom: 1px lightgray solid; 15 | margin-bottom: 16px; 16 | padding-bottom: 24px; 17 | box-sizing: content-box; 18 | } 19 | 20 | select, input { 21 | outline: none; 22 | border: none; 23 | background: rgba(0, 0, 0, 0.05); 24 | padding: 8px; 25 | border-radius: 4px; 26 | } 27 | 28 | select { 29 | margin-right: 16px; 30 | padding-right: 16px; 31 | } 32 | 33 | input { 34 | width: 48px; 35 | height: 32px; 36 | margin-right: 8px; 37 | } 38 | 39 | span { 40 | margin-right: 8px; 41 | } 42 | 43 | input[type="checkbox"] { 44 | margin-left: 16px; 45 | } 46 | } 47 | 48 | .device-emulator-container { 49 | display: flex; 50 | justify-content: center; 51 | flex-grow: 1; 52 | overflow: auto; 53 | } -------------------------------------------------------------------------------- /packages/react-device-frameset/scss/device-selector.scss: -------------------------------------------------------------------------------- 1 | .device-selector { 2 | margin-top: 40px; 3 | overflow: hidden; 4 | } 5 | 6 | .device-selector-container { 7 | display: flex; 8 | justify-content: center; 9 | padding: 20px 0; 10 | } 11 | 12 | .device-selector dl { 13 | display: grid; 14 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 15 | grid-template-rows: repeat(auto-fill, 1fr); 16 | gap: 10px; 17 | } 18 | 19 | .device-selector dl dt { 20 | font-size: 20px; 21 | font-weight: 600; 22 | letter-spacing: 0; 23 | font-style: normal; 24 | padding: 10px 0; 25 | grid-column: 1 / -1; 26 | display: flex; 27 | justify-content: space-between; 28 | } 29 | 30 | .device-selector dl dd { 31 | padding: 0; 32 | margin: 0; 33 | position: relative; 34 | cursor: pointer; 35 | text-align: left; 36 | flex-shrink: 0; 37 | flex-grow: 1; 38 | } 39 | 40 | .device-selector dl dd input { 41 | position: absolute; 42 | width: 100%; 43 | height: 100%; 44 | appearance: none; 45 | top: 0; 46 | z-index: -1; 47 | } 48 | .device-selector dl dd label { 49 | display: grid; 50 | grid-template-columns: 1fr 1fr; 51 | width: 100%; 52 | height: 100%; 53 | padding: 20px; 54 | text-align: center; 55 | border: 2px solid #d6d6d6; 56 | box-sizing: border-box; 57 | border-spacing: 0; 58 | overflow: hidden; 59 | cursor: pointer; 60 | background-color: hsla(0,0%,100%,.8); 61 | flex-direction: column; 62 | cursor: pointer; 63 | transition: all 0.5s ease; 64 | } 65 | .device-selector dl dd.active label, .device-selector dl dd.hover label { 66 | border-color: #0070c9; 67 | } 68 | 69 | .device-selector dl dd label span, .device-selector dl dt span { 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | color: black; 74 | background: #e9ecef; 75 | font-size: 12px; 76 | text-transform: uppercase; 77 | font-weight: 600; 78 | letter-spacing: 2px; 79 | padding: 10px 20px; 80 | border-radius: 30px; 81 | cursor: pointer; 82 | text-transform: uppercase; 83 | transition: all 0.5s ease; 84 | opacity: 0.8; 85 | transform: scale(0.8); 86 | } 87 | .device-selector dl dd label span.active, .device-selector dl dt span.active { 88 | opacity: 1; 89 | transform: scale(1); 90 | } 91 | .device-selector dl dd label div { 92 | display: flex; 93 | flex-direction: column; 94 | align-items: center; 95 | } 96 | .device-selector dl dd label div p { 97 | font-weight: 600; 98 | font-size: 17px; 99 | } 100 | .device-selector dl dd label ul { 101 | display: flex; 102 | flex-direction: row; 103 | flex-wrap: wrap; 104 | list-style: none; 105 | justify-content: flex-start; 106 | } 107 | .device-selector dl dd label ul li { 108 | display: block; 109 | height: 24px; 110 | width: 24px; 111 | margin: 0 8px 8px 0; 112 | border-radius: 50%; 113 | transition: all 0.5s ease; 114 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 115 | transform: scale(0.8); 116 | } 117 | .device-selector dl dd label ul li.active { 118 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3); 119 | transform: scale(1); 120 | } 121 | .device-selector dl dd label ul li.black { 122 | background-color: #2c2b2c; 123 | } 124 | .device-selector dl dd label ul li.silver { 125 | background-color: #f0f0f0; 126 | } 127 | .device-selector dl dd label ul li.gold { 128 | background-color: #f1e2d0; 129 | } 130 | .device-selector dl dd label ul li.white { 131 | background-color: #fff; 132 | } 133 | .device-selector dl dd label ul li.red { 134 | background-color: #f96b6c; 135 | } 136 | .device-selector dl dd label ul li.yellow { 137 | background-color: #f2dc60; 138 | } 139 | .device-selector dl dd label ul li.green { 140 | background-color: #97e563; 141 | } 142 | .device-selector dl dd label ul li.blue { 143 | background-color: #33a2db; 144 | } 145 | -------------------------------------------------------------------------------- /packages/react-device-frameset/scss/marvel-devices.scss: -------------------------------------------------------------------------------- 1 | .marvel-device{ 2 | display: inline-block; 3 | position: relative; 4 | box-sizing: content-box !important; 5 | transition: all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275); 6 | 7 | .screen { 8 | width: 100%; 9 | position: relative; 10 | height: 100%; 11 | z-index: 3; 12 | background: white; 13 | overflow: hidden; 14 | display: block; 15 | border-radius: 1px; 16 | box-shadow: 0 0 0 3px #111; 17 | } 18 | 19 | .top-bar, .bottom-bar { 20 | height: 3px; 21 | background: black; 22 | width: 100%; 23 | display: block; 24 | } 25 | 26 | .middle-bar { 27 | width: 3px; 28 | height: 4px; 29 | top: 0px; 30 | left: 90px; 31 | background: black; 32 | position: absolute; 33 | } 34 | 35 | &.iphone8{ 36 | width: 375px; 37 | height: 667px; 38 | padding: 105px 24px; 39 | background: #d9dbdc; 40 | border-radius: 56px; 41 | box-shadow:inset 0 0 3px 0 rgba(0, 0, 0, 0.2); 42 | 43 | &:before{ 44 | width: calc(100% - 12px); 45 | height: calc(100% - 12px); 46 | position: absolute; 47 | top: 6px; 48 | content: ''; 49 | left: 6px; 50 | border-radius: 50px; 51 | background: #f8f8f8; 52 | z-index: 1; 53 | } 54 | 55 | &:after{ 56 | width: calc(100% - 16px); 57 | height: calc(100% - 16px); 58 | position: absolute; 59 | top: 8px; 60 | content: ''; 61 | left: 8px; 62 | border-radius: 48px; 63 | box-shadow:inset 0 0 3px 0 rgba(0, 0, 0, 0.1), 64 | inset 0 0 6px 3px #FFFFFF; 65 | z-index: 2; 66 | } 67 | 68 | .home { 69 | border-radius: 100%; 70 | width: 68px; 71 | height: 68px; 72 | position: absolute; 73 | left: 50%; 74 | margin-left: -34px; 75 | bottom: 22px; 76 | z-index: 3; 77 | background: rgb(48,50,51); 78 | background: linear-gradient(135deg, rgba(48,50,51,1) 0%,rgba(181,183,185,1) 50%,rgba(240,242,242,1) 69%,rgba(48,50,51,1) 100%); 79 | 80 | &:before{ 81 | background: #f8f8f8; 82 | position: absolute; 83 | content: ''; 84 | border-radius: 100%; 85 | width: calc(100% - 8px); 86 | height: calc(100% - 8px); 87 | top: 4px; 88 | left: 4px; 89 | } 90 | } 91 | 92 | .top-bar{ 93 | height: 14px; 94 | background: #bfbfc0; 95 | position: absolute; 96 | top: 68px; 97 | left: 0; 98 | } 99 | 100 | .bottom-bar{ 101 | height: 14px; 102 | background: #bfbfc0; 103 | position: absolute; 104 | bottom: 68px; 105 | left: 0; 106 | } 107 | 108 | .sleep{ 109 | position: absolute; 110 | top: 190px; 111 | right: -4px; 112 | width: 4px; 113 | height: 66px; 114 | border-radius: 0px 2px 2px 0px; 115 | background: #d9dbdc; 116 | } 117 | 118 | .volume{ 119 | position: absolute; 120 | left: -4px; 121 | top: 188px; 122 | z-index: 0; 123 | height: 66px; 124 | width: 4px; 125 | border-radius: 2px 0px 0px 2px; 126 | background: #d9dbdc; 127 | 128 | &:before { 129 | position: absolute; 130 | left: 2px; 131 | top: -78px; 132 | height: 40px; 133 | width: 2px; 134 | border-radius: 2px 0px 0px 2px; 135 | background: inherit; 136 | content: ''; 137 | display: block; 138 | } 139 | 140 | &:after { 141 | position: absolute; 142 | left: 0px; 143 | top: 82px; 144 | height: 66px; 145 | width: 4px; 146 | border-radius: 2px 0px 0px 2px; 147 | background: inherit; 148 | content: ''; 149 | display: block; 150 | } 151 | } 152 | 153 | .camera { 154 | background: #3c3d3d; 155 | width: 12px; 156 | height: 12px; 157 | position: absolute; 158 | top: 24px; 159 | left: 50%; 160 | margin-left: -6px; 161 | border-radius: 100%; 162 | z-index: 3; 163 | } 164 | 165 | .sensor { 166 | background: #3c3d3d; 167 | width: 16px; 168 | height: 16px; 169 | position: absolute; 170 | top: 49px; 171 | left: 134px; 172 | z-index: 3; 173 | border-radius: 100%; 174 | } 175 | 176 | .speaker { 177 | background: #292728; 178 | width: 70px; 179 | height: 6px; 180 | position: absolute; 181 | top: 54px; 182 | left: 50%; 183 | margin-left: -35px; 184 | border-radius: 6px; 185 | z-index: 3; 186 | } 187 | 188 | &.gold{ 189 | background: #f9e7d3; 190 | 191 | .top-bar, .bottom-bar{ 192 | background: white; 193 | } 194 | 195 | .sleep, .volume{ 196 | background: #f9e7d3; 197 | } 198 | 199 | .home{ 200 | background: rgb(206,187,169); 201 | background: linear-gradient(135deg, rgba(206,187,169,1) 0%,rgba(249,231,211,1) 50%,rgba(206,187,169,1) 100%); 202 | } 203 | } 204 | 205 | &.black{ 206 | background: #464646; 207 | box-shadow: inset 0 0 3px 0 rgba(0, 0, 0, 0.7); 208 | 209 | &:before{ 210 | background: #080808; 211 | } 212 | 213 | &:after{ 214 | box-shadow: 215 | inset 0 0 3px 0 rgba(0, 0, 0, 0.1), 216 | inset 0 0 6px 3px #212121; 217 | } 218 | 219 | .top-bar, .bottom-bar{ 220 | background: #212121; 221 | } 222 | 223 | .volume, .sleep{ 224 | background: #464646; 225 | } 226 | 227 | .camera{ 228 | background: #080808; 229 | } 230 | 231 | .home{ 232 | background: rgb(8,8,8); 233 | background: linear-gradient(135deg, rgba(8,8,8,1) 0%,rgba(70,70,70,1) 50%,rgba(8,8,8,1) 100%); 234 | 235 | &:before{ 236 | background: #080808; 237 | } 238 | } 239 | 240 | } 241 | 242 | &.landscape{ 243 | padding: 24px 105px; 244 | height: 375px; 245 | width: 667px; 246 | 247 | .sleep{ 248 | top: 100%; 249 | border-radius: 0px 0px 2px 2px; 250 | right: 190px; 251 | height: 4px; 252 | width: 66px; 253 | } 254 | 255 | .volume { 256 | width: 66px; 257 | height: 4px; 258 | top: -4px; 259 | left: calc(100% - 188px - 66px); 260 | border-radius: 2px 2px 0px 0px; 261 | 262 | &:before { 263 | width: 40px; 264 | height: 2px; 265 | top: 2px; 266 | right: -78px; 267 | left: auto; 268 | border-radius: 2px 2px 0px 0px; 269 | } 270 | 271 | &:after{ 272 | left: -82px; 273 | width: 66px; 274 | height: 4px; 275 | top: 0; 276 | border-radius: 2px 2px 0px 0px; 277 | } 278 | } 279 | 280 | .top-bar{ 281 | width: 14px; 282 | height: 100%; 283 | left: calc(100% - 68px - 14px); 284 | top: 0; 285 | } 286 | 287 | .bottom-bar{ 288 | width: 14px; 289 | height: 100%; 290 | left: 68px; 291 | top: 0; 292 | } 293 | 294 | .home{ 295 | top: 50%; 296 | margin-top: -34px; 297 | margin-left: 0; 298 | left: 22px; 299 | } 300 | 301 | .sensor { 302 | top: 134px; 303 | left: calc(100% - 49px - 16px); 304 | } 305 | 306 | .speaker { 307 | height: 70px; 308 | width: 6px; 309 | left: calc(100% - 54px - 6px); 310 | top: 50%; 311 | margin-left: 0px; 312 | margin-top: -35px; 313 | } 314 | 315 | .camera { 316 | left: calc(100% - 32px); 317 | top: 50%; 318 | margin-left: 0px; 319 | margin-top: -5px; 320 | } 321 | } 322 | } 323 | 324 | &.iphone8plus{ 325 | width: 414px; 326 | height: 736px; 327 | padding: 112px 26px; 328 | background: #d9dbdc; 329 | border-radius: 56px; 330 | box-shadow:inset 0 0 3px 0 rgba(0, 0, 0, 0.2); 331 | 332 | &:before{ 333 | width: calc(100% - 12px); 334 | height: calc(100% - 12px); 335 | position: absolute; 336 | top: 6px; 337 | content: ''; 338 | left: 6px; 339 | border-radius: 50px; 340 | background: #f8f8f8; 341 | z-index: 1; 342 | } 343 | 344 | &:after{ 345 | width: calc(100% - 16px); 346 | height: calc(100% - 16px); 347 | position: absolute; 348 | top: 8px; 349 | content: ''; 350 | left: 8px; 351 | border-radius: 48px; 352 | box-shadow: inset 0 0 3px 0 rgba(0, 0, 0, 0.1), 353 | inset 0 0 6px 3px #FFFFFF; 354 | z-index: 2; 355 | } 356 | 357 | .home { 358 | border-radius: 100%; 359 | width: 68px; 360 | height: 68px; 361 | position: absolute; 362 | left: 50%; 363 | margin-left: -34px; 364 | bottom: 24px; 365 | z-index: 3; 366 | background: rgb(48,50,51); 367 | background: linear-gradient(135deg, rgba(48,50,51,1) 0%,rgba(181,183,185,1) 50%,rgba(240,242,242,1) 69%,rgba(48,50,51,1) 100%); 368 | 369 | &:before{ 370 | background: #f8f8f8; 371 | position: absolute; 372 | content: ''; 373 | border-radius: 100%; 374 | width: calc(100% - 8px); 375 | height: calc(100% - 8px); 376 | top: 4px; 377 | left: 4px; 378 | } 379 | } 380 | 381 | .top-bar{ 382 | height: 14px; 383 | background: #bfbfc0; 384 | position: absolute; 385 | top: 68px; 386 | left: 0; 387 | } 388 | 389 | .bottom-bar{ 390 | height: 14px; 391 | background: #bfbfc0; 392 | position: absolute; 393 | bottom: 68px; 394 | left: 0; 395 | } 396 | 397 | .sleep{ 398 | position: absolute; 399 | top: 190px; 400 | right: -4px; 401 | width: 4px; 402 | height: 66px; 403 | border-radius: 0px 2px 2px 0px; 404 | background: #d9dbdc; 405 | } 406 | 407 | .volume{ 408 | position: absolute; 409 | left: -4px; 410 | top: 188px; 411 | z-index: 0; 412 | height: 66px; 413 | width: 4px; 414 | border-radius: 2px 0px 0px 2px; 415 | background: #d9dbdc; 416 | 417 | &:before { 418 | position: absolute; 419 | left: 2px; 420 | top: -78px; 421 | height: 40px; 422 | width: 2px; 423 | border-radius: 2px 0px 0px 2px; 424 | background: inherit; 425 | content: ''; 426 | display: block; 427 | } 428 | 429 | &:after { 430 | position: absolute; 431 | left: 0px; 432 | top: 82px; 433 | height: 66px; 434 | width: 4px; 435 | border-radius: 2px 0px 0px 2px; 436 | background: inherit; 437 | content: ''; 438 | display: block; 439 | } 440 | } 441 | 442 | .camera { 443 | background: #3c3d3d; 444 | width: 12px; 445 | height: 12px; 446 | position: absolute; 447 | top: 29px; 448 | left: 50%; 449 | margin-left: -6px; 450 | border-radius: 100%; 451 | z-index: 3; 452 | } 453 | 454 | .sensor { 455 | background: #3c3d3d; 456 | width: 16px; 457 | height: 16px; 458 | position: absolute; 459 | top: 54px; 460 | left: 154px; 461 | z-index: 3; 462 | border-radius: 100%; 463 | } 464 | 465 | .speaker { 466 | background: #292728; 467 | width: 70px; 468 | height: 6px; 469 | position: absolute; 470 | top: 59px; 471 | left: 50%; 472 | margin-left: -35px; 473 | border-radius: 6px; 474 | z-index: 3; 475 | } 476 | 477 | &.gold{ 478 | background: #f9e7d3; 479 | 480 | .top-bar, .bottom-bar{ 481 | background: white; 482 | } 483 | 484 | .sleep, .volume{ 485 | background: #f9e7d3; 486 | } 487 | 488 | .home{ 489 | background: rgb(206,187,169); 490 | background: linear-gradient(135deg, rgba(206,187,169,1) 0%,rgba(249,231,211,1) 50%,rgba(206,187,169,1) 100%); 491 | } 492 | } 493 | 494 | &.black{ 495 | background: #464646; 496 | box-shadow:inset 0 0 3px 0 rgba(0, 0, 0, 0.7); 497 | 498 | &:before{ 499 | background: #080808; 500 | } 501 | 502 | &:after{ 503 | box-shadow: 504 | inset 0 0 3px 0 rgba(0, 0, 0, 0.1), 505 | inset 0 0 6px 3px #212121; 506 | } 507 | 508 | .top-bar, .bottom-bar{ 509 | background: #212121; 510 | } 511 | 512 | .volume, .sleep{ 513 | background: #464646; 514 | } 515 | 516 | .camera{ 517 | background: #080808; 518 | } 519 | 520 | .home{ 521 | background: rgb(8,8,8); 522 | background: linear-gradient(135deg, rgba(8,8,8,1) 0%,rgba(70,70,70,1) 50%,rgba(8,8,8,1) 100%); 523 | 524 | &:before{ 525 | background: #080808; 526 | } 527 | } 528 | 529 | } 530 | 531 | &.landscape{ 532 | padding: 26px 112px; 533 | height: 414px; 534 | width: 736px; 535 | 536 | .sleep{ 537 | top: 100%; 538 | border-radius: 0px 0px 2px 2px; 539 | right: 190px; 540 | height: 4px; 541 | width: 66px; 542 | } 543 | 544 | .volume { 545 | width: 66px; 546 | height: 4px; 547 | top: -4px; 548 | left: calc(100% - 188px - 66px); 549 | border-radius: 2px 2px 0px 0px; 550 | 551 | &:before { 552 | width: 40px; 553 | height: 2px; 554 | top: 2px; 555 | right: -78px; 556 | left: auto; 557 | border-radius: 2px 2px 0px 0px; 558 | } 559 | 560 | &:after{ 561 | left: -82px; 562 | width: 66px; 563 | height: 4px; 564 | top: 0; 565 | border-radius: 2px 2px 0px 0px; 566 | } 567 | } 568 | 569 | .top-bar{ 570 | width: 14px; 571 | height: 100%; 572 | left: calc(100% - 68px - 14px); 573 | top: 0; 574 | } 575 | 576 | .bottom-bar{ 577 | width: 14px; 578 | height: 100%; 579 | left: 68px; 580 | top: 0; 581 | } 582 | 583 | .home{ 584 | top: 50%; 585 | margin-top: -34px; 586 | margin-left: 0; 587 | left: 24px; 588 | } 589 | 590 | .sensor { 591 | top: 154px; 592 | left: calc(100% - 54px - 16px); 593 | } 594 | 595 | .speaker { 596 | height: 70px; 597 | width: 6px; 598 | left: calc(100% - 59px - 6px); 599 | top: 50%; 600 | margin-left: 0px; 601 | margin-top: -35px; 602 | } 603 | 604 | .camera { 605 | left: calc(100% - 29px); 606 | top: 50%; 607 | margin-left: 0px; 608 | margin-top: -5px; 609 | } 610 | } 611 | } 612 | 613 | &.iphone5s, &.iphone5c{ 614 | padding: 105px 22px; 615 | background: #2c2b2c; 616 | width: 320px; 617 | height: 568px; 618 | border-radius: 50px; 619 | 620 | &:before{ 621 | width: calc(100% - 8px); 622 | height: calc(100% - 8px); 623 | position: absolute; 624 | top: 4px; 625 | content: ''; 626 | left: 4px; 627 | border-radius: 46px; 628 | background: #1e1e1e; 629 | z-index: 1; 630 | } 631 | 632 | .sleep{ 633 | position: absolute; 634 | top: -4px; 635 | right: 60px; 636 | width: 60px; 637 | height: 4px; 638 | border-radius: 2px 2px 0px 0px; 639 | background: #282727; 640 | } 641 | 642 | .volume{ 643 | position: absolute; 644 | left: -4px; 645 | top: 180px; 646 | z-index: 0; 647 | height: 27px; 648 | width: 4px; 649 | border-radius: 2px 0px 0px 2px; 650 | background: #282727; 651 | 652 | &:before { 653 | position: absolute; 654 | left: 0px; 655 | top: -75px; 656 | height: 35px; 657 | width: 4px; 658 | border-radius: 2px 0px 0px 2px; 659 | background: inherit; 660 | content: ''; 661 | display: block; 662 | } 663 | 664 | &:after { 665 | position: absolute; 666 | left: 0px; 667 | bottom: -64px; 668 | height: 27px; 669 | width: 4px; 670 | border-radius: 2px 0px 0px 2px; 671 | background: inherit; 672 | content: ''; 673 | display: block; 674 | } 675 | } 676 | 677 | .camera { 678 | background: #3c3d3d; 679 | width: 10px; 680 | height: 10px; 681 | position: absolute; 682 | top: 32px; 683 | left: 50%; 684 | margin-left: -5px; 685 | border-radius: 5px; 686 | z-index: 3; 687 | } 688 | 689 | .sensor { 690 | background: #3c3d3d; 691 | width: 10px; 692 | height: 10px; 693 | position: absolute; 694 | top: 60px; 695 | left: 160px; 696 | z-index: 3; 697 | margin-left: -32px; 698 | border-radius: 5px; 699 | } 700 | 701 | .speaker { 702 | background: #292728; 703 | width: 64px; 704 | height: 10px; 705 | position: absolute; 706 | top: 60px; 707 | left: 50%; 708 | margin-left: -32px; 709 | border-radius: 5px; 710 | z-index: 3; 711 | } 712 | 713 | &.landscape{ 714 | padding: 22px 105px; 715 | height: 320px; 716 | width: 568px; 717 | 718 | .sleep{ 719 | right: -4px; 720 | top: calc(100% - 120px); 721 | height: 60px; 722 | width: 4px; 723 | border-radius: 0px 2px 2px 0px; 724 | } 725 | 726 | .volume { 727 | width: 27px; 728 | height: 4px; 729 | top: -4px; 730 | left: calc(100% - 180px); 731 | border-radius: 2px 2px 0px 0px; 732 | 733 | &:before { 734 | width: 35px; 735 | height: 4px; 736 | top: 0px; 737 | right: -75px; 738 | left: auto; 739 | border-radius: 2px 2px 0px 0px; 740 | } 741 | 742 | &:after{ 743 | bottom: 0px; 744 | left: -64px; 745 | z-index: 999; 746 | height: 4px; 747 | width: 27px; 748 | border-radius: 2px 2px 0px 0px; 749 | } 750 | } 751 | 752 | .sensor { 753 | top: 160px; 754 | left: calc(100% - 60px); 755 | margin-left: 0px; 756 | margin-top: -32px; 757 | } 758 | 759 | .speaker { 760 | height: 64px; 761 | width: 10px; 762 | left: calc(100% - 60px); 763 | top: 50%; 764 | margin-left: 0px; 765 | margin-top: -32px; 766 | } 767 | 768 | .camera { 769 | left: calc(100% - 32px); 770 | top: 50%; 771 | margin-left: 0px; 772 | margin-top: -5px; 773 | } 774 | } 775 | } 776 | 777 | &.iphone5s{ 778 | .home { 779 | border-radius: 36px; 780 | width: 68px; 781 | box-shadow: inset 0 0 0 4px #2c2b2c; 782 | height: 68px; 783 | position: absolute; 784 | left: 50%; 785 | margin-left: -34px; 786 | bottom: 19px; 787 | z-index: 3; 788 | } 789 | 790 | .top-bar{ 791 | top: 70px; 792 | position: absolute; 793 | left: 0; 794 | } 795 | 796 | .bottom-bar { 797 | bottom: 70px; 798 | position: absolute; 799 | left: 0; 800 | } 801 | 802 | &.landscape{ 803 | .home { 804 | left: 19px; 805 | bottom: 50%; 806 | margin-bottom: -34px; 807 | margin-left: 0px; 808 | } 809 | 810 | .top-bar { 811 | left: 70px; 812 | top: 0px; 813 | width: 3px; 814 | height: 100%; 815 | } 816 | 817 | .bottom-bar { 818 | right: 70px; 819 | left: auto; 820 | bottom: 0px; 821 | width: 3px; 822 | height: 100%; 823 | } 824 | } 825 | 826 | &.silver{ 827 | background: #bcbcbc; 828 | 829 | &:before{ 830 | background: #fcfcfc; 831 | } 832 | 833 | .volume, .sleep{ 834 | background: #d6d6d6; 835 | } 836 | 837 | .top-bar, .bottom-bar{ 838 | background: #eaebec; 839 | } 840 | 841 | .home{ 842 | box-shadow: inset 0 0 0 4px #bcbcbc; 843 | } 844 | } 845 | 846 | &.gold{ 847 | background: #f9e7d3; 848 | 849 | &:before{ 850 | background: #fcfcfc; 851 | } 852 | 853 | .volume, .sleep{ 854 | background: #f9e7d3; 855 | } 856 | 857 | .top-bar, .bottom-bar{ 858 | background: white; 859 | } 860 | 861 | .home{ 862 | box-shadow: inset 0 0 0 4px #f9e7d3; 863 | } 864 | } 865 | } 866 | 867 | &.iphone5c{ 868 | background: white; 869 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.2); 870 | 871 | .top-bar, .bottom-bar{ 872 | display: none; 873 | } 874 | 875 | .home{ 876 | background: #242324; 877 | border-radius: 36px; 878 | width: 68px; 879 | height: 68px; 880 | z-index: 3; 881 | position: absolute; 882 | left: 50%; 883 | margin-left: -34px; 884 | bottom: 19px; 885 | 886 | &:after{ 887 | width: 20px; 888 | height: 20px; 889 | border: 1px solid rgba(255, 255, 255, 0.1); 890 | border-radius: 4px; 891 | position: absolute; 892 | display: block; 893 | content: ''; 894 | top: 50%; 895 | left: 50%; 896 | margin-top: -11px; 897 | margin-left: -11px; 898 | } 899 | } 900 | 901 | &.landscape{ 902 | .home { 903 | left: 19px; 904 | bottom: 50%; 905 | margin-bottom: -34px; 906 | margin-left: 0px; 907 | } 908 | } 909 | 910 | .volume, .sleep{ 911 | background: #dddddd; 912 | } 913 | 914 | &.red{ 915 | background: #f96b6c; 916 | 917 | .volume, .sleep{ 918 | background: #ed5758; 919 | } 920 | } 921 | 922 | &.yellow{ 923 | background: #f2dc60; 924 | 925 | .volume, .sleep{ 926 | background: #e5ce4c; 927 | } 928 | } 929 | 930 | &.green{ 931 | background: #97e563; 932 | 933 | .volume, .sleep{ 934 | background: #85d94d; 935 | } 936 | } 937 | 938 | &.blue{ 939 | background: #33a2db; 940 | 941 | .volume, .sleep{ 942 | background: #2694cd; 943 | } 944 | } 945 | } 946 | 947 | &.iphone4s{ 948 | padding: 129px 27px; 949 | width: 320px; 950 | height: 480px; 951 | background: #686868; 952 | border-radius: 54px; 953 | 954 | &:before{ 955 | content: ''; 956 | width: calc(100% - 8px); 957 | height: calc(100% - 8px); 958 | position: absolute; 959 | top: 4px; 960 | left: 4px; 961 | z-index: 1; 962 | border-radius: 50px; 963 | background: #1e1e1e; 964 | } 965 | 966 | .top-bar { 967 | top: 60px; 968 | position: absolute; 969 | left: 0; 970 | } 971 | 972 | .bottom-bar { 973 | bottom: 90px; 974 | position: absolute; 975 | left: 0; 976 | } 977 | 978 | .camera { 979 | background: #3c3d3d; 980 | width: 10px; 981 | height: 10px; 982 | position: absolute; 983 | top: 72px; 984 | left: 134px; 985 | z-index: 3; 986 | margin-left: -5px; 987 | border-radius: 100%; 988 | } 989 | 990 | .speaker { 991 | background: #292728; 992 | width: 64px; 993 | height: 10px; 994 | position: absolute; 995 | top: 72px; 996 | left: 50%; 997 | z-index: 3; 998 | margin-left: -32px; 999 | border-radius: 5px; 1000 | } 1001 | 1002 | .sensor { 1003 | background: #292728; 1004 | width: 40px; 1005 | height: 10px; 1006 | position: absolute; 1007 | top: 36px; 1008 | left: 50%; 1009 | z-index: 3; 1010 | margin-left: -20px; 1011 | border-radius: 5px; 1012 | } 1013 | 1014 | .home { 1015 | background: #242324; 1016 | border-radius: 100%; 1017 | width: 72px; 1018 | height: 72px; 1019 | z-index: 3; 1020 | position: absolute; 1021 | left: 50%; 1022 | margin-left: -36px; 1023 | bottom: 30px; 1024 | 1025 | &:after { 1026 | width: 20px; 1027 | height: 20px; 1028 | border: 1px solid rgba(255, 255, 255, 0.1); 1029 | border-radius: 4px; 1030 | position: absolute; 1031 | display: block; 1032 | content: ''; 1033 | top: 50%; 1034 | left: 50%; 1035 | margin-top: -11px; 1036 | margin-left: -11px; 1037 | } 1038 | } 1039 | 1040 | .sleep { 1041 | position: absolute; 1042 | top: -4px; 1043 | right: 60px; 1044 | width: 60px; 1045 | height: 4px; 1046 | border-radius: 2px 2px 0px 0px; 1047 | background: #4D4D4D; 1048 | } 1049 | 1050 | .volume { 1051 | position: absolute; 1052 | left: -4px; 1053 | top: 160px; 1054 | height: 27px; 1055 | width: 4px; 1056 | border-radius: 2px 0px 0px 2px; 1057 | background: #4D4D4D; 1058 | 1059 | &:before { 1060 | position: absolute; 1061 | left: 0px; 1062 | top: -70px; 1063 | height: 35px; 1064 | width: 4px; 1065 | border-radius: 2px 0px 0px 2px; 1066 | background: inherit; 1067 | content: ''; 1068 | display: block; 1069 | } 1070 | 1071 | &:after { 1072 | position: absolute; 1073 | left: 0px; 1074 | bottom: -64px; 1075 | height: 27px; 1076 | width: 4px; 1077 | border-radius: 2px 0px 0px 2px; 1078 | background: inherit; 1079 | content: ''; 1080 | display: block; 1081 | } 1082 | } 1083 | 1084 | &.landscape{ 1085 | padding: 27px 129px; 1086 | height: 320px; 1087 | width: 480px; 1088 | 1089 | .bottom-bar { 1090 | left: 90px; 1091 | bottom: 0px; 1092 | height: 100%; 1093 | width: 3px; 1094 | } 1095 | 1096 | .top-bar { 1097 | left: calc(100% - 60px); 1098 | top: 0px; 1099 | height: 100%; 1100 | width: 3px; 1101 | } 1102 | 1103 | .camera { 1104 | top: 134px; 1105 | left: calc(100% - 72px); 1106 | margin-left: 0; 1107 | } 1108 | 1109 | .speaker{ 1110 | top: 50%; 1111 | margin-left: 0; 1112 | margin-top: -32px; 1113 | left: calc(100% - 72px); 1114 | width: 10px; 1115 | height: 64px; 1116 | } 1117 | 1118 | .sensor { 1119 | height: 40px; 1120 | width: 10px; 1121 | left: calc(100% - 36px); 1122 | top: 50%; 1123 | margin-left: 0; 1124 | margin-top: -20px; 1125 | } 1126 | 1127 | .home { 1128 | left: 30px; 1129 | bottom: 50%; 1130 | margin-left: 0; 1131 | margin-bottom: -36px; 1132 | } 1133 | 1134 | .sleep { 1135 | height: 60px; 1136 | width: 4px; 1137 | right: -4px; 1138 | top: calc(100% - 120px); 1139 | border-radius: 0px 2px 2px 0px; 1140 | } 1141 | 1142 | .volume { 1143 | top: -4px; 1144 | left: calc(100% - 187px); 1145 | height: 4px; 1146 | width: 27px; 1147 | border-radius: 2px 2px 0px 0px; 1148 | 1149 | &:before { 1150 | right: -70px; 1151 | left: auto; 1152 | top: 0px; 1153 | width: 35px; 1154 | height: 4px; 1155 | border-radius: 2px 2px 0px 0px; 1156 | } 1157 | 1158 | &:after{ 1159 | width: 27px; 1160 | height: 4px; 1161 | bottom: 0px; 1162 | left: -64px; 1163 | border-radius: 2px 2px 0px 0px; 1164 | } 1165 | } 1166 | } 1167 | 1168 | &.silver{ 1169 | background: #bcbcbc; 1170 | 1171 | &:before{ 1172 | background: #fcfcfc; 1173 | } 1174 | 1175 | .home{ 1176 | background: #fcfcfc; 1177 | box-shadow: inset 0 0 0 1px #bcbcbc; 1178 | 1179 | &:after{ 1180 | border: 1px solid rgba(0, 0, 0, 0.2); 1181 | } 1182 | } 1183 | 1184 | .volume, .sleep{ 1185 | background: #d6d6d6; 1186 | } 1187 | } 1188 | } 1189 | 1190 | &.nexus5{ 1191 | padding: 50px 15px 50px 15px; 1192 | width: 320px; 1193 | height: 568px; 1194 | background: #1e1e1e; 1195 | border-radius: 20px; 1196 | 1197 | &:before{ 1198 | border-radius: 600px / 50px; 1199 | background: inherit; 1200 | content: ''; 1201 | top: 0; 1202 | position: absolute; 1203 | height: 103.1%; 1204 | width: calc(100% - 26px); 1205 | top: 50%; 1206 | left: 50%; 1207 | transform: translateX(-50%) translateY(-50%); 1208 | } 1209 | 1210 | .top-bar{ 1211 | width: calc(100% - 8px); 1212 | height: calc(100% - 6px); 1213 | position: absolute; 1214 | top: 3px; 1215 | left: 4px; 1216 | border-radius: 20px; 1217 | background: #181818; 1218 | 1219 | &:before { 1220 | border-radius: 600px / 50px; 1221 | background: inherit; 1222 | content: ''; 1223 | top: 0; 1224 | position: absolute; 1225 | height: 103.0%; 1226 | width: calc(100% - 26px); 1227 | top: 50%; 1228 | left: 50%; 1229 | transform: translateX(-50%) translateY(-50%); 1230 | } 1231 | } 1232 | 1233 | .bottom-bar{ 1234 | display: none; 1235 | } 1236 | 1237 | .sleep{ 1238 | width: 3px; 1239 | position: absolute; 1240 | left: -3px; 1241 | top: 110px; 1242 | height: 100px; 1243 | background: inherit; 1244 | border-radius: 2px 0px 0px 2px; 1245 | } 1246 | 1247 | .volume{ 1248 | width: 3px; 1249 | position: absolute; 1250 | right: -3px; 1251 | top: 70px; 1252 | height: 45px; 1253 | background: inherit; 1254 | border-radius: 0px 2px 2px 0px; 1255 | } 1256 | 1257 | .camera { 1258 | background: #3c3d3d; 1259 | width: 10px; 1260 | height: 10px; 1261 | position: absolute; 1262 | top: 18px; 1263 | left: 50%; 1264 | z-index: 3; 1265 | margin-left: -5px; 1266 | border-radius: 100%; 1267 | 1268 | &:before { 1269 | background: #3c3d3d; 1270 | width: 6px; 1271 | height: 6px; 1272 | content: ''; 1273 | display: block; 1274 | position: absolute; 1275 | top: 2px; 1276 | left: -100px; 1277 | z-index: 3; 1278 | border-radius: 100%; 1279 | } 1280 | } 1281 | 1282 | &.landscape{ 1283 | padding: 15px 50px 15px 50px; 1284 | height: 320px; 1285 | width: 568px; 1286 | 1287 | &:before { 1288 | width: 103.1%; 1289 | height: calc(100% - 26px); 1290 | border-radius: 50px / 600px; 1291 | } 1292 | 1293 | .top-bar { 1294 | left: 3px; 1295 | top: 4px; 1296 | height: calc(100% - 8px); 1297 | width: calc(100% - 6px); 1298 | 1299 | &:before { 1300 | width: 103%; 1301 | height: calc(100% - 26px); 1302 | border-radius: 50px / 600px; 1303 | } 1304 | } 1305 | 1306 | .sleep{ 1307 | height: 3px; 1308 | width: 100px; 1309 | left: calc(100% - 210px); 1310 | top: -3px; 1311 | border-radius: 2px 2px 0px 0px; 1312 | } 1313 | 1314 | .volume{ 1315 | height: 3px; 1316 | width: 45px; 1317 | right: 70px; 1318 | top: 100%; 1319 | border-radius: 0px 0px 2px 2px; 1320 | } 1321 | 1322 | .camera { 1323 | top: 50%; 1324 | left: calc(100% - 18px); 1325 | margin-left: 0; 1326 | margin-top: -5px; 1327 | 1328 | &:before { 1329 | top: -100px; 1330 | left: 2px; 1331 | } 1332 | } 1333 | } 1334 | } 1335 | 1336 | &.s5{ 1337 | padding: 60px 18px; 1338 | border-radius: 42px; 1339 | width: 320px; 1340 | height: 568px; 1341 | background: #bcbcbc; 1342 | 1343 | &:before, &:after{ 1344 | width: calc(100% - 52px); 1345 | content: ''; 1346 | display: block; 1347 | height: 26px; 1348 | background: inherit; 1349 | position: absolute; 1350 | border-radius: 500px / 40px; 1351 | left: 50%; 1352 | transform: translateX(-50%); 1353 | } 1354 | 1355 | &:before{ 1356 | top: -7px; 1357 | } 1358 | 1359 | &:after{ 1360 | bottom: -7px; 1361 | } 1362 | 1363 | .bottom-bar{ 1364 | display: none; 1365 | } 1366 | 1367 | .top-bar{ 1368 | border-radius: 37px; 1369 | width: calc(100% - 10px); 1370 | height: calc(100% - 10px); 1371 | top: 5px; 1372 | left: 5px; 1373 | background: 1374 | radial-gradient(rgba(0, 0, 0, 0.02) 20%, transparent 60%) 0 0, 1375 | radial-gradient(rgba(0, 0, 0, 0.02) 20%, transparent 60%) 3px 3px; 1376 | background-color: white; 1377 | background-size: 4px 4px; 1378 | background-position: center; 1379 | z-index: 2; 1380 | position: absolute; 1381 | 1382 | &:before, &:after{ 1383 | width: calc(100% - 48px); 1384 | content: ''; 1385 | display: block; 1386 | height: 26px; 1387 | background: inherit; 1388 | position: absolute; 1389 | border-radius: 500px / 40px; 1390 | left: 50%; 1391 | transform: translateX(-50%); 1392 | } 1393 | 1394 | &:before{ 1395 | top: -7px; 1396 | } 1397 | 1398 | &:after{ 1399 | bottom: -7px; 1400 | } 1401 | } 1402 | 1403 | .sleep{ 1404 | width: 3px; 1405 | position: absolute; 1406 | left: -3px; 1407 | top: 100px; 1408 | height: 100px; 1409 | background: #cecece; 1410 | border-radius: 2px 0px 0px 2px; 1411 | } 1412 | 1413 | .speaker { 1414 | width: 68px; 1415 | height: 8px; 1416 | position: absolute; 1417 | top: 20px; 1418 | display: block; 1419 | z-index: 3; 1420 | left: 50%; 1421 | margin-left: -34px; 1422 | background-color: #bcbcbc; 1423 | background-position: top left; 1424 | border-radius: 4px; 1425 | } 1426 | 1427 | .sensor { 1428 | display: block; 1429 | position: absolute; 1430 | top: 20px; 1431 | right: 110px; 1432 | background: #3c3d3d; 1433 | border-radius: 100%; 1434 | width: 8px; 1435 | height: 8px; 1436 | z-index: 3; 1437 | 1438 | &:after { 1439 | display: block; 1440 | content: ''; 1441 | position: absolute; 1442 | top: 0px; 1443 | right: 12px; 1444 | background: #3c3d3d; 1445 | border-radius: 100%; 1446 | width: 8px; 1447 | height: 8px; 1448 | z-index: 3; 1449 | } 1450 | } 1451 | 1452 | .camera { 1453 | display: block; 1454 | position: absolute; 1455 | top: 24px; 1456 | right: 42px; 1457 | background: black; 1458 | border-radius: 100%; 1459 | width: 10px; 1460 | height: 10px; 1461 | z-index: 3; 1462 | 1463 | &:before{ 1464 | width: 4px; 1465 | height: 4px; 1466 | background: #3c3d3d; 1467 | border-radius: 100%; 1468 | position: absolute; 1469 | content: ''; 1470 | top: 50%; 1471 | left: 50%; 1472 | margin-top: -2px; 1473 | margin-left: -2px; 1474 | } 1475 | } 1476 | 1477 | .home { 1478 | position: absolute; 1479 | z-index: 3; 1480 | bottom: 17px; 1481 | left: 50%; 1482 | width: 70px; 1483 | height: 20px; 1484 | background: white; 1485 | border-radius: 18px; 1486 | display: block; 1487 | margin-left: -35px; 1488 | border: 2px solid black; 1489 | } 1490 | 1491 | &.landscape{ 1492 | padding: 18px 60px; 1493 | height: 320px; 1494 | width: 568px; 1495 | 1496 | &:before, &:after{ 1497 | height: calc(100% - 52px); 1498 | width: 26px; 1499 | border-radius: 40px / 500px; 1500 | transform: translateY(-50%); 1501 | } 1502 | 1503 | &:before { 1504 | top: 50%; 1505 | left: -7px; 1506 | } 1507 | 1508 | &:after { 1509 | top: 50%; 1510 | left: auto; 1511 | right: -7px; 1512 | } 1513 | 1514 | .top-bar{ 1515 | &:before, &:after{ 1516 | width: 26px; 1517 | height: calc(100% - 48px); 1518 | border-radius: 40px / 500px; 1519 | transform: translateY(-50%); 1520 | } 1521 | 1522 | &:before{ 1523 | right: -7px; 1524 | top: 50%; 1525 | left: auto; 1526 | } 1527 | 1528 | &:after{ 1529 | left: -7px; 1530 | top: 50%; 1531 | right: auto; 1532 | } 1533 | } 1534 | 1535 | .sleep{ 1536 | height: 3px; 1537 | width: 100px; 1538 | left: calc(100% - 200px); 1539 | top: -3px; 1540 | border-radius: 2px 2px 0px 0px; 1541 | } 1542 | 1543 | .speaker { 1544 | height: 68px; 1545 | width: 8px; 1546 | left: calc(100% - 20px); 1547 | top: 50%; 1548 | margin-left: 0; 1549 | margin-top: -34px; 1550 | } 1551 | 1552 | .sensor { 1553 | right: 20px; 1554 | top: calc(100% - 110px); 1555 | 1556 | &:after{ 1557 | left: -12px; 1558 | right: 0px; 1559 | } 1560 | } 1561 | 1562 | .camera { 1563 | top: calc(100% - 42px); 1564 | right: 24px; 1565 | } 1566 | 1567 | .home { 1568 | width: 20px; 1569 | height: 70px; 1570 | bottom: 50%; 1571 | margin-bottom: -35px; 1572 | margin-left: 0; 1573 | left: 17px; 1574 | } 1575 | } 1576 | 1577 | &.black{ 1578 | background: #1e1e1e; 1579 | 1580 | .speaker{ 1581 | background: black; 1582 | } 1583 | 1584 | .sleep{ 1585 | background: #1e1e1e; 1586 | } 1587 | 1588 | .top-bar{ 1589 | background: radial-gradient(rgba(0, 0, 0, 0.05) 20%, transparent 60%) 0 0, 1590 | radial-gradient(rgba(0, 0, 0, 0.05) 20%, transparent 60%) 3px 3px; 1591 | background-color: #2c2b2c; 1592 | background-size: 4px 4px; 1593 | } 1594 | 1595 | .home{ 1596 | background: #2c2b2c; 1597 | } 1598 | } 1599 | } 1600 | 1601 | &.lumia920{ 1602 | padding: 80px 35px 125px 35px; 1603 | background: #ffdd00; 1604 | width: 320px; 1605 | height: 533px; 1606 | border-radius: 40px / 3px; 1607 | 1608 | .bottom-bar{ 1609 | display: none; 1610 | } 1611 | 1612 | .top-bar{ 1613 | width: calc(100% - 24px); 1614 | height: calc(100% - 32px); 1615 | position: absolute; 1616 | top: 16px; 1617 | left: 12px; 1618 | border-radius: 24px; 1619 | background: black; 1620 | z-index: 1; 1621 | 1622 | &:before{ 1623 | background: #1e1e1e; 1624 | display: block; 1625 | content: ''; 1626 | width: calc(100% - 4px); 1627 | height: calc(100% - 4px); 1628 | top: 2px; 1629 | left: 2px; 1630 | position: absolute; 1631 | border-radius: 22px; 1632 | } 1633 | } 1634 | 1635 | .volume{ 1636 | width: 3px; 1637 | position: absolute; 1638 | top: 130px; 1639 | height: 100px; 1640 | background: #1e1e1e; 1641 | right: -3px; 1642 | border-radius: 0px 2px 2px 0px; 1643 | 1644 | &:before { 1645 | width: 3px; 1646 | position: absolute; 1647 | top: 190px; 1648 | content: ''; 1649 | display: block; 1650 | height: 50px; 1651 | background: inherit; 1652 | right: 0px; 1653 | border-radius: 0px 2px 2px 0px; 1654 | } 1655 | 1656 | &:after { 1657 | width: 3px; 1658 | position: absolute; 1659 | top: 460px; 1660 | content: ''; 1661 | display: block; 1662 | height: 50px; 1663 | background: inherit; 1664 | right: 0px; 1665 | border-radius: 0px 2px 2px 0px; 1666 | } 1667 | } 1668 | 1669 | .camera { 1670 | background: #3c3d3d; 1671 | width: 10px; 1672 | height: 10px; 1673 | position: absolute; 1674 | top: 34px; 1675 | right: 130px; 1676 | z-index: 5; 1677 | border-radius: 5px; 1678 | } 1679 | 1680 | .speaker { 1681 | background: #292728; 1682 | width: 64px; 1683 | height: 10px; 1684 | position: absolute; 1685 | top: 38px; 1686 | left: 50%; 1687 | margin-left: -32px; 1688 | border-radius: 5px; 1689 | z-index: 3; 1690 | } 1691 | 1692 | &.landscape{ 1693 | padding: 35px 80px 35px 125px; 1694 | height: 320px; 1695 | width: 568px; 1696 | border-radius: 2px / 100px; 1697 | 1698 | .top-bar{ 1699 | height: calc(100% - 24px); 1700 | width: calc(100% - 32px); 1701 | left: 16px; 1702 | top: 12px; 1703 | } 1704 | 1705 | .volume { 1706 | height: 3px; 1707 | right: 130px; 1708 | width: 100px; 1709 | top: 100%; 1710 | border-radius: 0px 0px 2px 2px; 1711 | 1712 | &:before{ 1713 | height: 3px; 1714 | right: 190px; 1715 | top: 0px; 1716 | width: 50px; 1717 | border-radius: 0px 0px 2px 2px; 1718 | } 1719 | 1720 | &:after{ 1721 | height: 3px; 1722 | right: 430px; 1723 | top: 0px; 1724 | width: 50px; 1725 | border-radius: 0px 0px 2px 2px; 1726 | } 1727 | } 1728 | 1729 | .camera { 1730 | right: 30px; 1731 | top: calc(100% - 140px); 1732 | } 1733 | 1734 | .speaker { 1735 | width: 10px; 1736 | height: 64px; 1737 | top: 50%; 1738 | margin-left: 0; 1739 | margin-top: -32px; 1740 | left: calc(100% - 48px); 1741 | } 1742 | } 1743 | 1744 | &.black{ 1745 | background: black; 1746 | } 1747 | 1748 | &.white{ 1749 | background: white; 1750 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.2); 1751 | } 1752 | 1753 | &.blue{ 1754 | background: #00acdd; 1755 | } 1756 | 1757 | &.red{ 1758 | background: #CC3E32; 1759 | } 1760 | } 1761 | 1762 | &.htc-one { 1763 | padding: 72px 25px 100px 25px; 1764 | width: 320px; 1765 | height: 568px; 1766 | background: #bebebe; 1767 | border-radius: 34px; 1768 | 1769 | &:before{ 1770 | content: ''; 1771 | display: block; 1772 | width: calc(100% - 4px); 1773 | height: calc(100% - 4px); 1774 | position: absolute; 1775 | top: 2px; 1776 | left: 2px; 1777 | background: #adadad; 1778 | border-radius: 32px; 1779 | } 1780 | 1781 | &:after{ 1782 | content: ''; 1783 | display: block; 1784 | width: calc(100% - 8px); 1785 | height: calc(100% - 8px); 1786 | position: absolute; 1787 | top: 4px; 1788 | left: 4px; 1789 | background: #eeeeee; 1790 | border-radius: 30px; 1791 | } 1792 | 1793 | .top-bar{ 1794 | width: calc(100% - 4px); 1795 | height: 635px; 1796 | position: absolute; 1797 | background: #424242; 1798 | top: 50px; 1799 | z-index: 1; 1800 | left: 2px; 1801 | 1802 | &:before{ 1803 | content: ''; 1804 | position: absolute; 1805 | width: calc(100% - 4px); 1806 | height: 100%; 1807 | position: absolute; 1808 | background: black; 1809 | top: 0px; 1810 | z-index: 1; 1811 | left: 2px; 1812 | } 1813 | } 1814 | 1815 | .bottom-bar{ 1816 | display: none; 1817 | } 1818 | 1819 | .speaker { 1820 | height: 16px; 1821 | width: 216px; 1822 | display: block; 1823 | position: absolute; 1824 | top: 22px; 1825 | z-index: 2; 1826 | left: 50%; 1827 | margin-left: -108px; 1828 | background: radial-gradient(#343434 25%, transparent 50%) 0 0, 1829 | radial-gradient(#343434 25%, transparent 50%) 4px 4px; 1830 | background-size: 4px 4px; 1831 | background-position: top left; 1832 | 1833 | &:after { 1834 | content: ''; 1835 | height: 16px; 1836 | width: 216px; 1837 | display: block; 1838 | position: absolute; 1839 | top: 676px; 1840 | z-index: 2; 1841 | left: 50%; 1842 | margin-left: -108px; 1843 | background: inherit; 1844 | } 1845 | } 1846 | 1847 | .camera { 1848 | display: block; 1849 | position: absolute; 1850 | top: 18px; 1851 | right: 38px; 1852 | background: #3c3d3d; 1853 | border-radius: 100%; 1854 | width: 24px; 1855 | height: 24px; 1856 | z-index: 3; 1857 | 1858 | &:before { 1859 | width: 8px; 1860 | height: 8px; 1861 | background: black; 1862 | border-radius: 100%; 1863 | position: absolute; 1864 | content: ''; 1865 | top: 50%; 1866 | left: 50%; 1867 | margin-top: -4px; 1868 | margin-left: -4px; 1869 | } 1870 | } 1871 | 1872 | .sensor { 1873 | display: block; 1874 | position: absolute; 1875 | top: 29px; 1876 | left: 60px; 1877 | background: #3c3d3d; 1878 | border-radius: 100%; 1879 | width: 8px; 1880 | height: 8px; 1881 | z-index: 3; 1882 | 1883 | &:after { 1884 | display: block; 1885 | content: ''; 1886 | position: absolute; 1887 | top: 0px; 1888 | right: 12px; 1889 | background: #3c3d3d; 1890 | border-radius: 100%; 1891 | width: 8px; 1892 | height: 8px; 1893 | z-index: 3; 1894 | } 1895 | } 1896 | 1897 | &.landscape{ 1898 | padding: 25px 72px 25px 100px; 1899 | height: 320px; 1900 | width: 568px; 1901 | 1902 | .top-bar{ 1903 | height: calc(100% - 4px); 1904 | width: 635px; 1905 | left: calc(100% - 685px); 1906 | top: 2px; 1907 | } 1908 | 1909 | .speaker { 1910 | width: 16px; 1911 | height: 216px; 1912 | left: calc(100% - 38px); 1913 | top: 50%; 1914 | margin-left: 0px; 1915 | margin-top: -108px; 1916 | 1917 | &:after { 1918 | width: 16px; 1919 | height: 216px; 1920 | left: calc(100% - 692px); 1921 | top: 50%; 1922 | margin-left: 0; 1923 | margin-top: -108px; 1924 | } 1925 | } 1926 | 1927 | .camera { 1928 | right: 18px; 1929 | top: calc(100% - 38px); 1930 | } 1931 | 1932 | .sensor { 1933 | left: calc(100% - 29px); 1934 | top: 60px; 1935 | 1936 | :after { 1937 | right: 0; 1938 | top: -12px; 1939 | } 1940 | } 1941 | } 1942 | } 1943 | 1944 | &.ipad{ 1945 | width: 576px; 1946 | height: 768px; 1947 | padding: 90px 25px; 1948 | background: #242324; 1949 | border-radius: 44px; 1950 | 1951 | &:before{ 1952 | width: calc(100% - 8px); 1953 | height: calc(100% - 8px); 1954 | position: absolute; 1955 | content: ''; 1956 | display: block; 1957 | top: 4px; 1958 | left: 4px; 1959 | border-radius: 40px; 1960 | background: #1e1e1e; 1961 | } 1962 | 1963 | .camera { 1964 | background: #3c3d3d; 1965 | width: 10px; 1966 | height: 10px; 1967 | position: absolute; 1968 | top: 44px; 1969 | left: 50%; 1970 | margin-left: -5px; 1971 | border-radius: 100%; 1972 | } 1973 | 1974 | .top-bar, .bottom-bar{ 1975 | display: none; 1976 | } 1977 | 1978 | .home { 1979 | background: #242324; 1980 | border-radius: 36px; 1981 | width: 50px; 1982 | height: 50px; 1983 | position: absolute; 1984 | left: 50%; 1985 | margin-left: -25px; 1986 | bottom: 22px; 1987 | 1988 | &:after { 1989 | width: 15px; 1990 | height: 15px; 1991 | margin-top: -8px; 1992 | margin-left: -8px; 1993 | border: 1px solid rgba(255, 255, 255, 0.1); 1994 | border-radius: 4px; 1995 | position: absolute; 1996 | display: block; 1997 | content: ''; 1998 | top: 50%; 1999 | left: 50%; 2000 | } 2001 | } 2002 | 2003 | &.landscape{ 2004 | height: 576px; 2005 | width: 768px; 2006 | padding: 25px 90px; 2007 | 2008 | .camera { 2009 | left: calc(100% - 44px); 2010 | top: 50%; 2011 | margin-left: 0; 2012 | margin-top: -5px; 2013 | } 2014 | 2015 | .home { 2016 | top: 50%; 2017 | left: 22px; 2018 | margin-left: 0; 2019 | margin-top: -25px; 2020 | } 2021 | } 2022 | 2023 | &.silver{ 2024 | background: #bcbcbc; 2025 | 2026 | &:before{ 2027 | background: #fcfcfc; 2028 | } 2029 | 2030 | .home{ 2031 | background: #fcfcfc; 2032 | box-shadow: inset 0 0 0 1px #bcbcbc; 2033 | 2034 | &:after{ 2035 | border: 1px solid rgba(0, 0, 0, 0.2); 2036 | } 2037 | } 2038 | } 2039 | } 2040 | 2041 | &.macbook { 2042 | width: 960px; 2043 | height: 600px; 2044 | padding: 44px 44px 76px; 2045 | margin: 0 auto; 2046 | background: #bebebe; 2047 | border-radius: 34px; 2048 | 2049 | &:before { 2050 | width: calc(100% - 8px); 2051 | height: calc(100% - 8px); 2052 | position: absolute; 2053 | content: ''; 2054 | display: block; 2055 | top: 4px; 2056 | left: 4px; 2057 | border-radius: 30px; 2058 | background: #1e1e1e; 2059 | } 2060 | 2061 | .top-bar { 2062 | width: calc(100% + 2 * 70px); 2063 | height: 40px; 2064 | position: absolute; 2065 | content: ''; 2066 | display: block; 2067 | top: 680px; 2068 | left: -70px; 2069 | border-bottom-left-radius: 90px 18px; 2070 | border-bottom-right-radius: 90px 18px; 2071 | background: #bebebe; 2072 | box-shadow: inset 0px -4px 13px 3px rgba(34, 34, 34, 0.6); 2073 | 2074 | &:before { 2075 | width: 100%; 2076 | height: 24px; 2077 | content: ''; 2078 | display: block; 2079 | top: 0; 2080 | left: 0; 2081 | background: #f0f0f0; 2082 | border-bottom: 2px solid #aaa; 2083 | border-radius: 5px; 2084 | position: relative; 2085 | } 2086 | 2087 | &:after { 2088 | width: 16%; 2089 | height: 14px; 2090 | content: ''; 2091 | display: block; 2092 | top: 0; 2093 | background: #ddd; 2094 | position: absolute; 2095 | margin-left: auto; 2096 | margin-right: auto; 2097 | left: 0; 2098 | right: 0; 2099 | border-radius: 0 0 20px 20px; 2100 | box-shadow: inset 0px -3px 10px #999; 2101 | } 2102 | } 2103 | 2104 | .bottom-bar { 2105 | background: transparent; 2106 | width: calc(100% + 2 * 70px); 2107 | height: 26px; 2108 | position: absolute; 2109 | content: ''; 2110 | display: block; 2111 | top: 680px; 2112 | left: -70px; 2113 | 2114 | &:before, 2115 | &:after { 2116 | height: calc(100% - 2px); 2117 | width: 80px; 2118 | content: ''; 2119 | display: block; 2120 | top: 0; 2121 | 2122 | position: absolute; 2123 | } 2124 | 2125 | &:before { 2126 | left: 0; 2127 | background: #f0f0f0; 2128 | background: linear-gradient(to right, #747474 0%, #c3c3c3 5%, #ebebeb 14%, #979797 41%, #f0f0f0 80%, #f0f0f0 100%, #f0f0f0 100%); 2129 | } 2130 | 2131 | &:after { 2132 | right: 0; 2133 | background: #f0f0f0; 2134 | background: linear-gradient(to right, #f0f0f0 0%, #f0f0f0 0%, #f0f0f0 20%, #979797 59%, #ebebeb 86%, #c3c3c3 95%, #747474 100%); 2135 | } 2136 | } 2137 | 2138 | .camera { 2139 | background: #3c3d3d; 2140 | width: 10px; 2141 | height: 10px; 2142 | position: absolute; 2143 | top: 20px; 2144 | left: 50%; 2145 | margin-left: -5px; 2146 | border-radius: 100%; 2147 | } 2148 | 2149 | .home { 2150 | display: none; 2151 | } 2152 | } 2153 | 2154 | &.iphone-x { 2155 | width: 375px; 2156 | height: 812px; 2157 | padding: 26px; 2158 | background: #fdfdfd; 2159 | box-shadow:inset 0 0 11px 0 black; 2160 | border-radius: 66px; 2161 | 2162 | .overflow { 2163 | width: 100%; 2164 | height: 100%; 2165 | position: absolute; 2166 | top: 0; 2167 | left: 0; 2168 | border-radius: 66px; 2169 | overflow: hidden; 2170 | } 2171 | 2172 | .shadow { 2173 | border-radius: 100%; 2174 | width: 90px; 2175 | height: 90px; 2176 | position: absolute; 2177 | background: radial-gradient(ellipse at center, rgba(0,0,0,0.6) 0%,rgba(255,255,255,0) 60%); 2178 | } 2179 | 2180 | .shadow--tl { 2181 | top: -20px; 2182 | left: -20px; 2183 | } 2184 | 2185 | .shadow--tr { 2186 | top: -20px; 2187 | right: -20px; 2188 | } 2189 | 2190 | .shadow--bl { 2191 | bottom: -20px; 2192 | left: -20px; 2193 | } 2194 | 2195 | .shadow--br { 2196 | bottom: -20px; 2197 | right: -20px; 2198 | } 2199 | 2200 | &:before{ 2201 | width: calc(100% - 10px); 2202 | height: calc(100% - 10px); 2203 | position: absolute; 2204 | top: 5px; 2205 | content: ''; 2206 | left: 5px; 2207 | border-radius: 61px; 2208 | background: black; 2209 | z-index: 1; 2210 | } 2211 | 2212 | .inner-shadow{ 2213 | width: calc(100% - 20px); 2214 | height: calc(100% - 20px); 2215 | position: absolute; 2216 | top: 10px; 2217 | overflow: hidden; 2218 | left: 10px; 2219 | border-radius: 56px; 2220 | box-shadow: inset 0 0 15px 0 rgba(white, 0.66); 2221 | z-index: 1; 2222 | 2223 | &:before{ 2224 | box-shadow:inset 0 0 20px 0 #FFFFFF; 2225 | width: 100%; 2226 | height: 116%; 2227 | position: absolute; 2228 | top: -8%; 2229 | content: ''; 2230 | left: 0; 2231 | border-radius: 200px / 112px; 2232 | z-index: 2; 2233 | } 2234 | } 2235 | 2236 | .screen { 2237 | border-radius: 40px; 2238 | box-shadow: none; 2239 | } 2240 | 2241 | .top-bar, .bottom-bar { 2242 | width: 100%; 2243 | position: absolute; 2244 | height: 8px; 2245 | background: rgba(black, 0.1); 2246 | left: 0; 2247 | } 2248 | 2249 | .top-bar { 2250 | top: 80px; 2251 | } 2252 | 2253 | .bottom-bar { 2254 | bottom: 80px; 2255 | } 2256 | 2257 | .volume, .volume:before, .volume:after, .sleep { 2258 | width: 3px; 2259 | background: #b5b5b5; 2260 | position: absolute; 2261 | } 2262 | 2263 | .volume { 2264 | left: -3px; 2265 | top: 116px; 2266 | height: 32px; 2267 | 2268 | &:before { 2269 | height: 62px; 2270 | top: 62px; 2271 | content: ''; 2272 | left: 0; 2273 | } 2274 | 2275 | &:after { 2276 | height: 62px; 2277 | top: 140px; 2278 | content: ''; 2279 | left: 0; 2280 | } 2281 | } 2282 | 2283 | .sleep { 2284 | height: 96px; 2285 | top: 200px; 2286 | right: -3px; 2287 | } 2288 | 2289 | .camera { 2290 | width: 6px; 2291 | height: 6px; 2292 | top: 9px; 2293 | border-radius: 100%; 2294 | position: absolute; 2295 | left: 154px; 2296 | background: #0d4d71; 2297 | } 2298 | 2299 | .speaker { 2300 | height: 6px; 2301 | width: 60px; 2302 | left: 50%; 2303 | position: absolute; 2304 | top: 9px; 2305 | margin-left: -30px; 2306 | background: #171818; 2307 | border-radius: 6px; 2308 | } 2309 | 2310 | .notch { 2311 | position: absolute; 2312 | width: 210px; 2313 | height: 30px; 2314 | top: 26px; 2315 | left: 108px; 2316 | z-index: 4; 2317 | background: black; 2318 | border-bottom-left-radius: 24px; 2319 | border-bottom-right-radius: 24px; 2320 | 2321 | &:before, &:after { 2322 | content: ''; 2323 | height: 8px; 2324 | position: absolute; 2325 | top: 0; 2326 | width: 8px; 2327 | } 2328 | 2329 | &:after { 2330 | background: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%); 2331 | left: -8px; 2332 | } 2333 | 2334 | &:before { 2335 | background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%); 2336 | right: -8px; 2337 | } 2338 | } 2339 | 2340 | &.landscape { 2341 | height: 375px; 2342 | width: 812px; 2343 | 2344 | .top-bar, .bottom-bar { 2345 | width: 8px; 2346 | height: 100%; 2347 | top: 0; 2348 | } 2349 | 2350 | .top-bar { 2351 | left: 80px; 2352 | } 2353 | 2354 | .bottom-bar { 2355 | right: 80px; 2356 | bottom: auto; 2357 | left: auto; 2358 | } 2359 | 2360 | .volume, .volume:before, .volume:after, .sleep { 2361 | height: 3px; 2362 | } 2363 | 2364 | .inner-shadow:before { 2365 | height: 100%; 2366 | width: 116%; 2367 | left: -8%; 2368 | top: 0; 2369 | border-radius: 112px / 200px; 2370 | } 2371 | 2372 | .volume { 2373 | bottom: -3px; 2374 | top: auto; 2375 | left: 116px; 2376 | width: 32px; 2377 | 2378 | &:before { 2379 | width: 62px; 2380 | left: 62px; 2381 | top: 0; 2382 | } 2383 | 2384 | &:after { 2385 | width: 62px; 2386 | left: 140px; 2387 | top: 0; 2388 | } 2389 | } 2390 | 2391 | .sleep { 2392 | width: 96px; 2393 | left: 200px; 2394 | top: -3px; 2395 | right: auto; 2396 | } 2397 | 2398 | .camera { 2399 | left: 9px; 2400 | bottom: 154px; 2401 | top: auto; 2402 | } 2403 | 2404 | .speaker { 2405 | width: 6px; 2406 | height: 60px; 2407 | left: 9px; 2408 | top: 50%; 2409 | margin-top: -30px; 2410 | margin-left: 0; 2411 | } 2412 | 2413 | .notch { 2414 | height: 210px; 2415 | width: 30px; 2416 | left: 26px; 2417 | bottom: 108px; 2418 | top: auto; 2419 | border-top-right-radius: 24px; 2420 | border-bottom-right-radius: 24px; 2421 | border-bottom-left-radius: 0; 2422 | 2423 | &:before, &:after { 2424 | left: 0; 2425 | } 2426 | 2427 | &:after { 2428 | background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%); 2429 | bottom: -8px; 2430 | top: auto; 2431 | } 2432 | 2433 | &:before { 2434 | background: radial-gradient(circle at top right, transparent 0, transparent 70%, black 70%, black 100%); 2435 | top: -8px; 2436 | } 2437 | } 2438 | } 2439 | } 2440 | 2441 | &.note8 { 2442 | width: 400px; 2443 | height: 822px; 2444 | background: black; 2445 | border-radius: 34px; 2446 | padding: 45px 10px; 2447 | 2448 | .overflow { 2449 | width: 100%; 2450 | height: 100%; 2451 | position: absolute; 2452 | top: 0; 2453 | left: 0; 2454 | border-radius: 34px; 2455 | overflow: hidden; 2456 | } 2457 | 2458 | .speaker { 2459 | height: 8px; 2460 | width: 56px; 2461 | left: 50%; 2462 | position: absolute; 2463 | top: 25px; 2464 | margin-left: -28px; 2465 | background: #171818; 2466 | z-index: 1; 2467 | border-radius: 8px; 2468 | } 2469 | 2470 | .camera { 2471 | height: 18px; 2472 | width: 18px; 2473 | left: 86px; 2474 | position: absolute; 2475 | top: 18px; 2476 | background: #212b36; 2477 | z-index: 1; 2478 | border-radius: 100%; 2479 | 2480 | &:before{ 2481 | content: ''; 2482 | height: 8px; 2483 | width: 8px; 2484 | left: -22px; 2485 | position: absolute; 2486 | top: 5px; 2487 | background: #212b36; 2488 | z-index: 1; 2489 | border-radius: 100%; 2490 | } 2491 | } 2492 | 2493 | .sensors { 2494 | height: 10px; 2495 | width: 10px; 2496 | left: 120px; 2497 | position: absolute; 2498 | top: 22px; 2499 | background: #1d233b; 2500 | z-index: 1; 2501 | border-radius: 100%; 2502 | 2503 | &:before{ 2504 | content: ''; 2505 | height: 10px; 2506 | width: 10px; 2507 | left: 18px; 2508 | position: absolute; 2509 | top: 0; 2510 | background: #1d233b; 2511 | z-index: 1; 2512 | border-radius: 100%; 2513 | } 2514 | } 2515 | 2516 | .more-sensors { 2517 | height: 16px; 2518 | width: 16px; 2519 | left: 285px; 2520 | position: absolute; 2521 | top: 18px; 2522 | background: #33244a; 2523 | box-shadow: 0 0 0 2px rgba(white, 0.1); 2524 | z-index: 1; 2525 | border-radius: 100%; 2526 | 2527 | &:before{ 2528 | content: ''; 2529 | height: 11px; 2530 | width: 11px; 2531 | left: 40px; 2532 | position: absolute; 2533 | top: 4px; 2534 | background: #214a61; 2535 | z-index: 1; 2536 | border-radius: 100%; 2537 | } 2538 | } 2539 | 2540 | .sleep { 2541 | width: 2px; 2542 | height: 56px; 2543 | background: black; 2544 | position: absolute; 2545 | top: 288px; 2546 | right: -2px; 2547 | } 2548 | 2549 | .volume { 2550 | width: 2px; 2551 | height: 120px; 2552 | background: black; 2553 | position: absolute; 2554 | top: 168px; 2555 | left: -2px; 2556 | 2557 | &:before { 2558 | content: ''; 2559 | top: 168px; 2560 | width: 2px; 2561 | position: absolute; 2562 | left: 0; 2563 | background: black; 2564 | height: 56px; 2565 | } 2566 | } 2567 | 2568 | .inner { 2569 | width: 100%; 2570 | height: calc(100% - 8px); 2571 | position: absolute; 2572 | top: 2px; 2573 | content: ''; 2574 | left: 0px; 2575 | border-radius: 34px; 2576 | border-top: 2px solid #9fa0a2; 2577 | border-bottom: 2px solid #9fa0a2; 2578 | background: black; 2579 | z-index: 1; 2580 | box-shadow: inset 0 0 6px 0 rgba(white, 0.5); 2581 | } 2582 | 2583 | .shadow{ 2584 | box-shadow: 2585 | inset 0 0 60px 0 white, 2586 | inset 0 0 30px 0 rgba(white, 0.5), 2587 | 0 0 20px 0 white, 2588 | 0 0 20px 0 rgba(white, 0.5); 2589 | height: 101%; 2590 | position: absolute; 2591 | top: -0.5%; 2592 | content: ''; 2593 | width: calc(100% - 20px); 2594 | left: 10px; 2595 | border-radius: 38px; 2596 | z-index: 5; 2597 | pointer-events: none; 2598 | } 2599 | 2600 | .screen { 2601 | border-radius: 14px; 2602 | box-shadow: none; 2603 | } 2604 | 2605 | &.landscape { 2606 | height: 400px; 2607 | width: 822px; 2608 | padding: 10px 45px; 2609 | 2610 | .speaker { 2611 | height: 56px; 2612 | width: 8px; 2613 | top: 50%; 2614 | margin-top: -28px; 2615 | margin-left: 0; 2616 | right: 25px; 2617 | left: auto; 2618 | } 2619 | 2620 | .camera { 2621 | top: 86px; 2622 | right: 18px; 2623 | left: auto; 2624 | 2625 | &:before { 2626 | top: -22px; 2627 | left: 5px; 2628 | } 2629 | } 2630 | 2631 | .sensors { 2632 | top: 120px; 2633 | right: 22px; 2634 | left: auto; 2635 | 2636 | &:before { 2637 | top: 18px; 2638 | left: 0; 2639 | } 2640 | } 2641 | 2642 | .more-sensors { 2643 | top: 285px; 2644 | right: 18px; 2645 | left: auto; 2646 | 2647 | &:before { 2648 | top: 40px; 2649 | left: 4px; 2650 | } 2651 | } 2652 | 2653 | .sleep { 2654 | bottom: -2px; 2655 | top: auto; 2656 | right: 288px; 2657 | width: 56px; 2658 | height: 2px; 2659 | } 2660 | 2661 | .volume { 2662 | width: 120px; 2663 | height: 2px; 2664 | top: -2px; 2665 | right: 168px; 2666 | left: auto; 2667 | 2668 | &:before { 2669 | right: 168px; 2670 | left: auto; 2671 | top: 0; 2672 | width: 56px; 2673 | height: 2px; 2674 | } 2675 | } 2676 | 2677 | .inner { 2678 | height: 100%; 2679 | width: calc(100% - 8px); 2680 | left: 2px; 2681 | top: 0; 2682 | border-top: 0; 2683 | border-bottom: 0; 2684 | border-left: 2px solid #9fa0a2; 2685 | border-right: 2px solid #9fa0a2; 2686 | } 2687 | 2688 | .shadow { 2689 | width: 101%; 2690 | height: calc(100% - 20px); 2691 | left: -0.5%; 2692 | top: 10px; 2693 | } 2694 | } 2695 | } 2696 | } -------------------------------------------------------------------------------- /packages/react-device-frameset/src/DeviceEmulator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react' 2 | import { DeviceOptions, DeviceFramesetProps } from './DeviceFrameset' 3 | import { DeviceName, DeviceNames } from './DeviceOptions' 4 | 5 | export type DeviceEmulatorProps = React.HTMLAttributes & { 6 | banDevices?: DeviceName[] 7 | children: (props: DeviceFramesetProps) => React.ReactNode, 8 | value?: DeviceFramesetProps, 9 | onChange?: (deviceConfig: DeviceFramesetProps) => void, 10 | } 11 | 12 | const zooms = [ 13 | 0.5, 0.75, 1, 1.25, 1.5 14 | ] 15 | 16 | export const DeviceEmulator = React.memo(function DeviceEmulator ({ children, value, onChange, banDevices = [], ...divProps }) { 17 | const deviceNames = useMemo(() => DeviceNames.filter(devName => !banDevices.includes(devName)), [banDevices]) 18 | const [deviceName, setDeviceName] = useState(deviceNames[0] ?? '') 19 | 20 | const selectedDeviceName = useMemo(() => value?.device ?? deviceName, [value, deviceName]) 21 | 22 | const handleSelectChange = useCallback( 23 | (event: React.ChangeEvent) => { 24 | const newDeviceName = event.target.value as DeviceName 25 | if (!deviceNames.includes(newDeviceName)) throw new Error(`Invalid device name for ${newDeviceName}`) 26 | 27 | setDeviceName(newDeviceName) 28 | }, 29 | [deviceNames], 30 | ) 31 | 32 | const [selectedZoom, setSelectedZoom] = useState(zooms[2]) 33 | 34 | const handleSelectZoomChange = useCallback( 35 | (event: React.ChangeEvent) => { 36 | const newZoom = +event.target.value 37 | if (!zooms.includes(newZoom)) throw new Error(`Invalid device zoom for ${newZoom}`) 38 | 39 | setSelectedZoom(newZoom) 40 | }, 41 | [], 42 | ) 43 | 44 | const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[selectedDeviceName], [selectedDeviceName]) 45 | 46 | const firstColor = useMemo(() => colors[0], [colors]) 47 | 48 | const [isLandscape, setIsLandscape] = useState(undefined) 49 | 50 | const isLandscapeChecked = useMemo(() => hasLandscape ? isLandscape : undefined, [hasLandscape, isLandscape]) 51 | 52 | const handleIsLandscapeChange = useCallback( 53 | () => { 54 | if (!hasLandscape) return 55 | 56 | setIsLandscape(is => !is) 57 | }, 58 | [hasLandscape] 59 | ) 60 | 61 | const deviceFramesetProps = useMemo( 62 | () => value ?? ({ 63 | device: selectedDeviceName, 64 | color: firstColor, 65 | landscape: isLandscapeChecked, 66 | width, 67 | height, 68 | zoom: selectedZoom, 69 | }) as DeviceFramesetProps, 70 | [value, selectedDeviceName, firstColor, isLandscapeChecked, width, height, selectedZoom], 71 | ) 72 | 73 | useEffect( 74 | () => { 75 | onChange?.(deviceFramesetProps) 76 | }, 77 | [onChange, deviceFramesetProps] 78 | ) 79 | 80 | return ( 81 |
82 |
83 | 91 | 92 | x 93 | 94 | 102 | 103 | 104 |
105 | 106 |
107 | {children(deviceFramesetProps)} 108 |
109 |
110 | ) 111 | }) 112 | -------------------------------------------------------------------------------- /packages/react-device-frameset/src/DeviceFrameset.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | 3 | import type { DeviceFramesetProps } from './DeviceOptions' 4 | import { DeviceOptions } from './DeviceOptions' 5 | 6 | export { DeviceOptions, DeviceFramesetProps } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | function omit, K extends string> (item: T, keys: K[]): Omit { 10 | const clone = { ...item } 11 | for (const key of keys) { 12 | delete clone[key] 13 | } 14 | return clone 15 | } 16 | 17 | export const DeviceFrameset = React.memo( 18 | function DeviceFrameset(props) { 19 | const { children, device, width, height, zoom, ...restProps } = props 20 | const divProps = omit(restProps, ['landscape', 'color']) 21 | 22 | const color = 'color' in props ? props.color : undefined 23 | const landscape = 'landscape' in props ? props.landscape : undefined 24 | 25 | const style = useMemo( 26 | () => (landscape && DeviceOptions[device].hasLandscape) 27 | ? ({ width: height, height: width, transform: zoom !== undefined ? `scale(${zoom})` : undefined }) 28 | : ({ width, height, transform: zoom !== undefined ? `scale(${zoom})` : undefined }), 29 | [width, height, landscape, device, zoom], 30 | ) 31 | 32 | return ( 33 |
38 |
39 | {device === 'Galaxy Note 8' ?
40 |
41 |
: null} 42 | {device === 'iPhone X' ?
43 |
44 |
45 |
: null} 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {device === 'iPhone X' ?
56 |
57 |
58 |
59 |
60 |
: null} 61 |
62 |
63 | {children} 64 |
65 |
66 |
67 |
68 | ) 69 | } 70 | ); 71 | -------------------------------------------------------------------------------- /packages/react-device-frameset/src/DeviceOptions.ts: -------------------------------------------------------------------------------- 1 | import type { Compute, OptionField, OmitFieldByType } from './helper' 2 | 3 | type DeviceType = { 4 | device: Device, 5 | colors: Colors, 6 | hasLandscape: boolean, 7 | width?: number, 8 | height?: number, 9 | } 10 | 11 | export const defineDevice = < 12 | Device extends string, 13 | Colors extends readonly string[], 14 | Def extends DeviceType 15 | >(definition: Def) => definition 16 | 17 | export const DeviceOptions = { 18 | ['iPhone X']: defineDevice({ 19 | device: 'iphone-x', 20 | colors: [] as const, 21 | hasLandscape: true, 22 | width: 375, 23 | height: 812, 24 | }), 25 | ['iPhone 8']: defineDevice({ 26 | device: 'iphone8', 27 | colors: ['black', 'silver', 'gold'] as const, 28 | hasLandscape: true, 29 | width: 375, 30 | height: 667, 31 | }), 32 | ['iPhone 8 Plus']: defineDevice({ 33 | device: 'iphone8plus', 34 | colors: ['black', 'silver', 'gold'] as const, 35 | hasLandscape: true, 36 | width: 414, 37 | height: 736, 38 | }), 39 | ['iPhone 5s']: defineDevice({ 40 | device: 'iphone5s', 41 | colors: ['black', 'silver', 'gold'] as const, 42 | hasLandscape: true, 43 | width: 320, 44 | height: 568, 45 | }), 46 | ['iPhone 5c']: defineDevice({ 47 | device: 'iphone5c', 48 | colors: ['white', 'red', 'yellow', 'green', 'blue'] as const, 49 | hasLandscape: true, 50 | width: 320, 51 | height: 568, 52 | }), 53 | ['iPhone 4s']: defineDevice({ 54 | device: 'iphone4s', 55 | colors: ['black', 'silver'] as const, 56 | hasLandscape: true, 57 | width: 320, 58 | height: 480, 59 | }), 60 | ['Galaxy Note 8']: defineDevice({ 61 | device: 'note8', 62 | colors: [] as const, 63 | hasLandscape: true, 64 | width: 400, 65 | height: 822, 66 | }), 67 | ['Nexus 5']: defineDevice({ 68 | device: 'nexus5', 69 | colors: [] as const, 70 | hasLandscape: true, 71 | width: 320, 72 | height: 568, 73 | }), 74 | ['Lumia 920']: defineDevice({ 75 | device: 'lumia920', 76 | colors: ['black', 'white', 'yellow', 'red', 'blue'] as const, 77 | hasLandscape: true, 78 | width: 320, 79 | height: 533, 80 | }), 81 | ['Samsung Galaxy S5']: defineDevice({ 82 | device: 's5', 83 | colors: ['white', 'black'] as const, 84 | hasLandscape: true, 85 | width: 320, 86 | height: 568, 87 | }), 88 | ['HTC One']: defineDevice({ 89 | device: 'nexus5', 90 | colors: [] as const, 91 | hasLandscape: true, 92 | width: 320, 93 | height: 568, 94 | }), 95 | ['iPad Mini']: defineDevice({ 96 | device: 'ipad', 97 | colors: ['black', 'silver'] as const, 98 | hasLandscape: true, 99 | width: 576, 100 | height: 768, 101 | }), 102 | ['MacBook Pro']: defineDevice({ 103 | device: 'macbook', 104 | colors: [] as const, 105 | hasLandscape: false, 106 | width: 960, 107 | height: 600, 108 | }), 109 | } 110 | 111 | export type DeviceName = keyof typeof DeviceOptions 112 | 113 | export const DeviceNames = Object.keys(DeviceOptions) as DeviceName[] 114 | 115 | type DevicesType>> = { 116 | [key in keyof R]: Compute>> 124 | }[keyof R] 125 | 126 | export type DeviceFramesetProps = DevicesType & React.HTMLAttributes 127 | -------------------------------------------------------------------------------- /packages/react-device-frameset/src/DeviceSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState, useCallback } from 'react' 2 | import { DeviceOptions, DeviceFramesetProps } from './DeviceFrameset' 3 | import { DeviceName, DeviceNames } from './DeviceOptions' 4 | 5 | export type DeviceSelectorProps = React.HTMLAttributes & { 6 | banDevices?: DeviceName[], 7 | children: (props: DeviceFramesetProps) => React.ReactNode, 8 | value?: DeviceName, 9 | onChange?: (deviceName: DeviceName) => void, 10 | } 11 | 12 | export const DeviceSelector = React.memo(function DeviceSelector ({ children, value, onChange, banDevices = [], ...divProps }) { 13 | const deviceNames = useMemo(() => DeviceNames.filter(devName => !banDevices.includes(devName)), [banDevices]) 14 | const [deviceName, setDeviceName] = useState(deviceNames[0] ?? '') 15 | const selectedDeviceName = useMemo(() => value ?? deviceName, [value, deviceName]) 16 | 17 | const handleSelectChange = useCallback( 18 | (event: React.MouseEvent) => { 19 | 20 | const newDeviceName = event.currentTarget.dataset['deviceName'] as DeviceName 21 | if (!deviceNames.includes(newDeviceName)) throw new Error(`Invalid device name for ${newDeviceName}`) 22 | 23 | onChange?.(newDeviceName) 24 | setDeviceName(newDeviceName) 25 | }, 26 | [deviceNames, onChange], 27 | ) 28 | 29 | const [showMenu, setShowMenu] = useState(true) 30 | 31 | const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[selectedDeviceName], [selectedDeviceName]) 32 | 33 | const firstColor = useMemo(() => colors[0], [colors]) 34 | 35 | const [selectedColor, setSelectedColor] = useState(firstColor) 36 | 37 | const handleColorChange = useCallback( 38 | (event: React.MouseEvent) => { 39 | 40 | const newDeviceColor = event.currentTarget.dataset['deviceColor'] as typeof colors[number] 41 | 42 | setSelectedColor(newDeviceColor) 43 | }, 44 | [], 45 | ) 46 | 47 | useEffect(() => { setSelectedColor(firstColor) }, [firstColor]) 48 | 49 | const [isLandscape, setIsLandscape] = useState(undefined) 50 | 51 | const isLandscapeChecked = useMemo(() => hasLandscape ? isLandscape : undefined, [hasLandscape, isLandscape]) 52 | 53 | const handleIsLandscapeChange = useCallback( 54 | () => { 55 | if (!hasLandscape) return 56 | 57 | setIsLandscape(is => !is) 58 | }, 59 | [hasLandscape] 60 | ) 61 | 62 | const deviceFramesetProps = useMemo( 63 | () => ({ 64 | device: selectedDeviceName, 65 | color: selectedColor, 66 | landscape: isLandscapeChecked, 67 | width, 68 | height, 69 | }) as DeviceFramesetProps, 70 | [selectedDeviceName, selectedColor, isLandscapeChecked, width, height], 71 | ) 72 | 73 | return ( 74 |
75 |
76 |
77 |

The Chosen: {selectedDeviceName}

78 | setShowMenu(is => !is)} 81 | > 82 | show all devices 83 | 84 |
85 | {showMenu && deviceNames.map((devName) => ( 86 |
92 | 93 | 119 |
120 | ))} 121 |
122 | 123 |
124 | {children(deviceFramesetProps)} 125 |
126 |
127 | ) 128 | }) -------------------------------------------------------------------------------- /packages/react-device-frameset/src/Zoomable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | 3 | export type ZoomableType = React.DetailedHTMLProps< 4 | React.HTMLAttributes, 5 | HTMLDivElement 6 | > 7 | 8 | const steps = [ 9 | 0, 10 | 1 / 4, 11 | 1 / 3, 12 | 1 / 2, 13 | 2 / 3, 14 | 3 / 4, 15 | 4 / 5, 16 | 9 / 10, 17 | 1, 18 | 11 / 10, 19 | 5 / 4, 20 | 3 / 2, 21 | 7 / 4, 22 | 2 / 1, 23 | 5 / 2, 24 | 3 / 1, 25 | 4 / 1, 26 | 5 / 1, 27 | Infinity, 28 | ] 29 | const toCurrIndex = (value: number) => 30 | steps.findIndex((step) => step > value) - 1 31 | const toPrevIndex = (value: number) => 32 | steps.findIndex((step) => step > value) - 2 33 | const toNextIndex = (value: number) => steps.findIndex((step) => step > value) 34 | const checkReachBound = (value: number) => { 35 | if (value === steps[0]) return steps[1] 36 | if (value === steps[steps.length - 1]) return steps[steps.length - 2] 37 | 38 | return value 39 | } 40 | const toCurr = (value: number) => { 41 | return checkReachBound(steps[toCurrIndex(value)]) 42 | } 43 | const toPrev = (value: number) => { 44 | return checkReachBound(steps[toPrevIndex(value)]) 45 | } 46 | const toNext = (value: number) => { 47 | return checkReachBound(steps[toNextIndex(value)]) 48 | } 49 | 50 | export const Zoomable = React.memo(function Zoomable({ 51 | children, 52 | ...props 53 | }) { 54 | const [[width, height], setSize] = useState([1, 1]) 55 | const [[clientWidth, clientHeight], setClientSize] = useState([width, height]) 56 | const [container, setContainer] = useState(null) 57 | const [zoomContainer, setZoomContainer] = useState( 58 | null 59 | ) 60 | const scale = useMemo(() => { 61 | const [scaleX, scaleY] = [width / clientWidth, height / clientHeight] 62 | 63 | return Math.min(scaleX, scaleY, 1) 64 | }, [width, height, clientWidth, clientHeight]) 65 | 66 | const [localScale, setLocaleScale] = useState(null) 67 | 68 | const realScale = useMemo( 69 | () => toCurr(localScale ?? scale), 70 | [localScale, scale] 71 | ) 72 | 73 | const allowScroll = useMemo( 74 | () => clientWidth / realScale < width || clientHeight / realScale < height, 75 | [clientHeight, clientWidth, realScale, width, height] 76 | ) 77 | 78 | const transformStyle = useMemo(() => { 79 | return { transform: `scale(${realScale})` } 80 | }, [realScale]) 81 | 82 | useEffect(() => { 83 | if (zoomContainer) { 84 | const syncClientSize = (el: Element) => { 85 | const { clientWidth, clientHeight } = el 86 | clientWidth && 87 | clientHeight && 88 | setClientSize([clientWidth, clientHeight]) 89 | } 90 | 91 | syncClientSize(zoomContainer) 92 | 93 | const option: ResizeObserverOptions = {} 94 | 95 | const observe: ResizeObserverCallback = (entries) => { 96 | for (const entry of entries) { 97 | syncClientSize(entry.target) 98 | } 99 | } 100 | 101 | const observer = new ResizeObserver(observe) 102 | 103 | observer.observe(zoomContainer, option) 104 | 105 | return () => observer.disconnect() 106 | } 107 | }, [zoomContainer]) 108 | useEffect(() => { 109 | if (container) { 110 | const { clientWidth, clientHeight } = container 111 | setSize([clientWidth, clientHeight]) 112 | } 113 | }, [container]) 114 | 115 | return ( 116 |
117 |
128 |
132 | {children} 133 |
134 |
135 |
136 | setLocaleScale(toPrev(realScale))}>- 137 | {Math.round(realScale * 100)}% 138 | setLocaleScale(toNext(realScale))}>+ 139 |
140 |
141 | ) 142 | }) 143 | -------------------------------------------------------------------------------- /packages/react-device-frameset/src/helper.ts: -------------------------------------------------------------------------------- 1 | export type KeysOfType = { 2 | [key in keyof T]: [F] extends [T[key]] ? [T[key]] extends [F] ? key : never : never 3 | }[keyof T] 4 | export type KeysOfSubType = { 5 | [key in keyof T]: [F] extends [T[key]] ? key : never 6 | }[keyof T] 7 | export type OmitFieldByType = Omit> 8 | export type OptionField = Omit> & Partial>> 9 | export type Compute = { [K in keyof A]: A[K] } & unknown 10 | -------------------------------------------------------------------------------- /packages/react-device-frameset/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DeviceFrameset'; 2 | export * from './DeviceSelector'; 3 | export * from './DeviceEmulator'; 4 | export * from './Zoomable' -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "jsx": "react-jsx", 5 | "lib": ["ES2015", "DOM"], 6 | "declaration": true, 7 | "moduleResolution": "Node", 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "noImplicitAny": true, 12 | "strictFunctionTypes": true, 13 | "strictBindCallApply": true, 14 | "strictPropertyInitialization": true, 15 | "alwaysStrict": true, 16 | "noErrorTruncation": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------