├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .ncurc.json
├── .prettierrc.js
├── .watchmanconfig
├── LICENSE
├── README.md
├── babel.config.js
├── metro.config.js
├── package.json
├── react-native-scaled-layout.jpg
├── src
└── index.tsx
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.test.*
2 | **/*.snap.*
3 | **/*.d.ts
4 |
5 | metro.config.js
6 | /dist
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@dooboo/eslint-config',
4 | plugins: ['react-hooks'],
5 | rules: {
6 | 'react-hooks/rules-of-hooks': 'error',
7 | 'react-hooks/exhaustive-deps': 'error',
8 | 'react/display-name': 0,
9 | '@typescript-eslint/ban-ts-ignore': 0,
10 | '@typescript-eslint/no-explicit-any': 0,
11 | 'no-unused-expressions': 'off',
12 | 'no-extra-parens': 0,
13 | '@typescript-eslint/no-empty-function': 0,
14 | 'max-len': [
15 | 'error',
16 | {
17 | code: 120,
18 | ignoreRegExpLiterals: true,
19 | ignoreComments: true,
20 | ignoreUrls: true,
21 | ignoreStrings: true,
22 | },
23 | ],
24 | '@typescript-eslint/no-non-null-assertion': 0,
25 | '@typescript-eslint/no-empty-interface': 1,
26 | '@typescript-eslint/camelcase': 0,
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # node.js
2 | #
3 | dist/
4 | node_modules/
5 | npm-debug.log
6 | yarn-error.log
7 | package-lock.json
8 | yarn.lock
9 | .idea/
10 |
--------------------------------------------------------------------------------
/.ncurc.json:
--------------------------------------------------------------------------------
1 | {
2 | "upgrade": true,
3 | "reject": [
4 | "react",
5 | "react-dom",
6 | "react-test-renderer"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'all',
3 | arrowParens: 'always',
4 | singleQuote: true,
5 | jsxSingleQuote: false,
6 | printWidth: 120,
7 | };
8 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 dooboolab
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-native-scaled-layout
2 | 
3 |
4 | 
5 |
6 | ### Sorry to Api changes in 1.1.0 😢
7 |
8 | ## [Detail Article's Link 🔗](https://medium.com/mj-studio/stop-hesitate-to-support-the-ipad-in-react-native-1040edc318cd)
9 |
10 | ### Flexible, Scalable layout dimensions, font sizes for React Native
11 |
12 |
13 |
14 | `@mj-studio/react-native-scaled-layout` is using monkey-patch feature in javascript(typescript) and augmentation syntax in typescript.
15 |
16 | ## Contents 🏆
17 |
18 | * [Install](#install-)
19 | * [Usage](#usage-)
20 | - [0. Configure Environment](#0-configure-your-environment-or-wanted-behavior)
21 | - [1. Use with extension functions](#1-number-type-augmentationextension)
22 | - [2. ScaledText Component](#2-scaledtext-component)
23 | * [Calculation](#calculation-)
24 | * [Trouble Shooting](#trouble-shooting%EF%B8%8F)
25 | * [Todo](#todo-)
26 | * [Change Logs](#change-logs-)
27 | ## Install 💠
28 |
29 | ```
30 | npm i @mj-studio/react-native-scaled-layout
31 | ```
32 |
33 | or
34 |
35 | ```
36 | yarn add @mj-studio/react-native-scaled-layout
37 | ```
38 |
39 | ## Usage 📌
40 |
41 | ### 0. Configure your environment or wanted behavior
42 |
43 | ❕ If `@mj-studio/react-native-scaled-layout` is not imported for side-effect, then `TypeError` will be invoked.
44 |
45 | _`index.js`_
46 | ```ts
47 | import { initScaledSettings } from '@mj-studio/react-native-scaled-layout';
48 | ...
49 | initScaledSettings(375, { min: 0.5, max: 1.5 }, { min: 0.75, max: 1.35 }, 14);
50 | ```
51 | | Params | Type | Default | Required |
52 | | ------------------- |---------|---------|----------|
53 | | designSpecWidth | number | 375 | false |
54 | | dimenScaleRange | `{ min: number; max: number }` | [0.5, 1.5] | false |
55 | | fontScaleRange | `{ min: number; max: number }` | [0.75, 1.3] | false |
56 | | defaultFontSize | number | 14 | false |
57 |
58 | ### 1. Number type Augmentation(Extension)
59 |
60 | ```ts
61 | // calculated with width length of design spec
62 | // clamped with dimenScaleRange min, max value
63 | (36).scaled() /* or */ (36).d()
64 |
65 | // calculated with width length of design spec
66 | // clamped with fontScaleRange min, max value
67 | (24).fontScaled() /* or */ (24).f()
68 | ```
69 |
70 | Example in `ViewStyle`
71 |
72 | ```tsx
73 | style={{
74 | width: (100).d(),
75 | height: (210).d() + safeAreaBottom,
76 | borderRadius: (16).d(),
77 | justifyContent: 'center',
78 | paddingBottom: safeAreaBottom + (24).d(),
79 | }}
80 | ```
81 |
82 | ### 2. ScaledText Component
83 |
84 | ```tsx
85 | // automatically adjusted with (14).fontScaled()
86 | My Text
87 |
88 | // ignore calculated font scale
89 | // fixed with 28(14 * 2)
90 | My Text
91 | ```
92 |
93 | `react-native-scaled-layout` is also compatible with [Styled Component](https://styled-components.com/)
94 |
95 | ```tsx
96 | export const BoldText = styled(ScaledText)`
97 | font-family: ${fonts.NotoSansKRBold};
98 | `;
99 | ...
100 | const TutorialText = styled(BoldText)`
101 | left: ${(20).d()}px;
102 | right: ${(20).d()}px;
103 | position: absolute;
104 | font-size: 24px; // automatically adjust font size with (24).fontScaled()
105 | color: ${({ theme }): string => theme.white};
106 | `;
107 | ```
108 |
109 | ## Calculation 📐
110 |
111 | The following is the implementation of `initScaledSettings`
112 |
113 | ```ts
114 | /**
115 | * Set initial configuration for scaled layout behavior. If your height of design guideline spec is less than width, invert 1st, 2nd params.
116 | * @param designSpecWidth your design width viewport width(zeplin, pigma etc...). If your design viewport is 375 x 1000 then 375 is a right value.
117 | * @param dimenScaleRange dimension scale factor minimum & maximum range. default is [0.5, 1.5]..
118 | * @param fontScaleRange font scale factor minimum & maximum range. default is [0.75, 1.3].
119 | * @param defaultFontsize default `` fontSize. default is 12.
120 | *
121 | * @example
122 | * ```ts
123 | * initScaledSettings(375, 812, {min: 0.5, max: 1.5}, {min: 0.75, max: 1.3}, 12);
124 | * ```
125 | */
126 | export function initScaledSettings(
127 | designSpecWidth = 375,
128 | dimenScaleRange: { min: number; max: number } = { min: 0.5, max: 1.5 },
129 | fontScaleRange: { min: number; max: number } = { min: 0.75, max: 1.3 },
130 | defaultFontsize = 12,
131 | ): void {
132 | dimenRatio = minLength / designSpecWidth;
133 |
134 | dimenScale = clamp(dimenRatio, dimenScaleRange.min, dimenScaleRange.max);
135 |
136 | fontScale =
137 | dimenScale >= 1 ? Math.min(dimenScale, fontScaleRange.max) : Math.max(dimenScale * dimenScale, fontScaleRange.min);
138 | _FONT_SCALE_ = fontScale;
139 | _defaultFontSize = defaultFontsize;
140 |
141 | /* eslint-disable no-extend-native */
142 | Number.prototype.scaled = function scaled(): number {
143 | return Math.round((this as number) * dimenScale);
144 | };
145 | Number.prototype.fontScaled = function fontScaled(): number {
146 | return Math.round((this as number) * fontScale);
147 | };
148 | Number.prototype.d = function d(): number {
149 | return (this as number).scaled();
150 | };
151 | Number.prototype.f = function f(): number {
152 | return (this as number).fontScaled();
153 | };
154 | /* eslint-enable no-extend-native */
155 | }
156 | initScaledSettings();
157 | ```
158 |
159 | ## Trouble Shooting❗️
160 |
161 | ### 1. TypeError: 40.d is not a function
162 |
163 | Please put `import '@mj-studio/react-native-scaled-layout'` to top of `index.js` or top of file which in `setupFiles` list of `jest.config.js`
164 |
165 |
166 | ## Todo ✅
167 |
168 | * Create `ScaledView`, `ScaledTextInput`, `ScaledTouchableXXX` like `ScaledText`
169 |
170 | ## Change Logs 🔧
171 | * 1.2.0
172 | - Change `customFontScale` logic in `ScaledText`
173 | * 1.1.7
174 | - Fix `ScaledText` font size scaling issue. Previously, the `fontSize` property is multiplied to float not integer.
175 | * 1.1.6
176 | - The values are calculated with `ceil()` instead of `round()`.
177 | * 1.1.4
178 | - `minimumFontSize` prop is added in `ScaledText`
179 | * 1.1.2
180 | - `dimenWidthScaled()`, `dimenHeightScaled()`, `w()`, `h()` are removed
181 | - `designSpecHeight` parameter is removed from `initScaledSettings`
182 | * 1.1.1
183 | - `FontScale` is calculated with design spec width length not design spec diagonal length
184 | * 1.1.0 (Sorry to API changes)
185 | - `dimenScaled()` is renamed to `scaled()`
186 | - New `number` type augmentation `dimenWidthScaled()`, `dimenHeightScaled()`
187 | - Add simple alias for functions `d()`, `f()`, `w()`, `h()`
188 | * 1.0.6
189 | - Apply round for fixing showing weird line because of floating number dimension
190 |
191 | ### feel free your fork or any PR! Thanks
192 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset', '@babel/preset-typescript'],
3 | sourceMaps: 'inline',
4 | plugins: [
5 | [
6 | '@babel/plugin-proposal-decorators',
7 | {
8 | legacy: true,
9 | },
10 | ],
11 | [
12 | '@babel/plugin-transform-runtime',
13 | {
14 | helpers: true,
15 | regenerator: false,
16 | },
17 | ],
18 | '@babel/proposal-object-rest-spread',
19 | ],
20 | };
21 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('metro-config');
2 |
3 | module.exports = (async () => {
4 | const {
5 | resolver: { sourceExts, assetExts },
6 | } = await getDefaultConfig();
7 | return {
8 | transformer: {
9 | babelTransformerPath: require.resolve('react-native-svg-transformer'),
10 | },
11 | resolver: {
12 | assetExts: assetExts.filter((ext) => ext !== 'svg'),
13 | sourceExts: [...sourceExts, 'svg'],
14 | },
15 | };
16 | })();
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mj-studio/react-native-scaled-layout",
3 | "version": "1.2.0",
4 | "description": "Flexible, Scalable layout dimensions, font sizes for React Native",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist/**/*"
9 | ],
10 | "private": false,
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/mym0404/react-native-scaled-layout"
14 | },
15 | "keywords": [],
16 | "author": "MJ Studio ",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/mym0404/react-native-scaled-layout/issues"
20 | },
21 | "homepage": "https://github.com/mym0404/react-native-scaled-layout",
22 | "scripts": {
23 | "prepare": "yarn run check",
24 | "check": "yarn lint && tsc",
25 | "lint": "eslint src --ext .ts,.tsx,.js,.jsx"
26 | },
27 | "peerDependencies": {
28 | "react": "*",
29 | "react-native": ">=0.50"
30 | },
31 | "dependencies": {},
32 | "devDependencies": {
33 | "@babel/cli": "^7.8.4",
34 | "@babel/core": "^7.8.7",
35 | "@babel/plugin-proposal-class-properties": "^7.8.3",
36 | "@babel/plugin-proposal-decorators": "7.8.3",
37 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3",
38 | "@babel/plugin-transform-runtime": "7.8.3",
39 | "@babel/preset-typescript": "^7.8.3",
40 | "@babel/runtime": "^7.8.7",
41 | "@dooboo/eslint-config": "^0.3.8",
42 | "@types/react": "^16.9.23",
43 | "@types/react-native": "^0.61.21",
44 | "babel-preset-react-native": "^5.0.2",
45 | "eslint": "^6.8.0",
46 | "eslint-plugin-react-hooks": "^2.5.0",
47 | "import-sort-style-eslint": "^6.0.0",
48 | "jetifier": "^1.6.5",
49 | "metro-react-native-babel-preset": "^0.58.0",
50 | "prettier": "^2.0.5",
51 | "prettier-plugin-import-sort": "^0.0.4",
52 | "typescript": "3.8.3"
53 | },
54 | "importSort": {
55 | ".js, .jsx": {
56 | "style": "eslint",
57 | "options": {}
58 | },
59 | ".ts, .tsx": {
60 | "parser": "typescript",
61 | "style": "eslint",
62 | "options": {}
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/react-native-scaled-layout.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mj-studio-library/react-native-scaled-layout/626737bee33fafb2bcc5eae86b6378c64ca59eeb/react-native-scaled-layout.jpg
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { Dimensions, StyleProp, StyleSheet, Text, TextProps, TextStyle } from 'react-native';
2 | import React, { ReactElement, ReactNode } from 'react';
3 |
4 | const window = Dimensions.get('window');
5 | const minLength = Math.min(window.width, window.height);
6 | let dimenRatio: number;
7 | let dimenScale: number;
8 | let fontScale: number;
9 | export let _FONT_SCALE_: number;
10 | let _defaultFontSize: number;
11 |
12 | declare global {
13 | interface Number {
14 | scaled: () => number;
15 | fontScaled: () => number;
16 | d: () => number;
17 | f: () => number;
18 | }
19 | }
20 |
21 | function clamp(value: number, min: number, max: number): number {
22 | if (value < min) return min;
23 | if (value > max) return max;
24 | return value;
25 | }
26 |
27 | /**
28 | * Set initial configuration for scaled layout behavior. If your height of design guideline spec is less than width, invert 1st, 2nd params.
29 | * @param designSpecWidth your design width viewport width(zeplin, pigma etc...). If your design viewport is 375 x 1000 then 375 is a right value.
30 | * @param dimenScaleRange dimension scale factor minimum & maximum range. default is [0.5, 1.5]..
31 | * @param fontScaleRange font scale factor minimum & maximum range. default is [0.75, 1.3].
32 | * @param defaultFontsize default `` fontSize. default is 12.
33 | *
34 | * @example
35 | * ```ts
36 | * initScaledSettings(375, 812, {min: 0.5, max: 1.5}, {min: 0.75, max: 1.3}, 12);
37 | * ```
38 | */
39 | export function initScaledSettings(
40 | designSpecWidth = 375,
41 | dimenScaleRange: { min: number; max: number } = { min: 0.5, max: 1.5 },
42 | fontScaleRange: { min: number; max: number } = { min: 0.75, max: 1.3 },
43 | defaultFontsize = 12,
44 | ): void {
45 | dimenRatio = minLength / designSpecWidth;
46 |
47 | dimenScale = clamp(dimenRatio, dimenScaleRange.min, dimenScaleRange.max);
48 |
49 | fontScale =
50 | dimenScale >= 1 ? Math.min(dimenScale, fontScaleRange.max) : Math.max(dimenScale * dimenScale, fontScaleRange.min);
51 | _FONT_SCALE_ = fontScale;
52 | _defaultFontSize = defaultFontsize;
53 |
54 | /* eslint-disable no-extend-native */
55 | Number.prototype.scaled = function scaled(): number {
56 | return Math.ceil((this as number) * dimenScale);
57 | };
58 | Number.prototype.fontScaled = function fontScaled(): number {
59 | return Math.ceil((this as number) * fontScale);
60 | };
61 | Number.prototype.d = function d(): number {
62 | return (this as number).scaled();
63 | };
64 | Number.prototype.f = function f(): number {
65 | return (this as number).fontScaled();
66 | };
67 | /* eslint-enable no-extend-native */
68 | }
69 | initScaledSettings();
70 |
71 | type ScaledTextProps = {
72 | style?: StyleProp;
73 | children?: ReactNode;
74 | allowFontScaling?: boolean;
75 | customFontScale?: number;
76 | minimumFontSize?: number;
77 | } & TextProps;
78 | export const ScaledText = React.forwardRef(
79 | (props: ScaledTextProps, ref: React.Ref): ReactElement => {
80 | const { style, children, allowFontScaling, customFontScale, minimumFontSize, ...rest } = props;
81 |
82 | let fontSize = StyleSheet.flatten(style)?.fontSize ?? _defaultFontSize;
83 |
84 | if (allowFontScaling !== false) {
85 | if (typeof customFontScale === 'number' && customFontScale > 0) {
86 | fontSize = Math.ceil(fontSize * customFontScale);
87 | }
88 | fontSize = Math.ceil(fontSize * _FONT_SCALE_);
89 | }
90 |
91 | if (minimumFontSize) {
92 | fontSize = Math.ceil(Math.max(minimumFontSize, fontSize));
93 | }
94 |
95 | return (
96 |
97 | {children}
98 |
99 | );
100 | },
101 | );
102 |
103 | ScaledText.displayName = 'ScaledText';
104 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2020"],
4 | "jsx": "react",
5 | "noResolve": false,
6 | "noImplicitAny": false,
7 | "removeComments": false,
8 | "sourceMap": true,
9 | "module": "commonjs",
10 | "moduleResolution": "node",
11 | "target": "es5",
12 | "skipLibCheck": true,
13 | "allowSyntheticDefaultImports": true,
14 | "declaration": true,
15 | "esModuleInterop": true,
16 | "outDir": "dist" /* Redirect output structure to the directory. */
17 | },
18 | "compileOnSave": true,
19 | // "include": ["src/index.ts"],
20 | "exclude": ["dist", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------