├── .nvmrc ├── docs ├── .nojekyll ├── img │ ├── ff.png │ └── chrome.png ├── src │ └── docs.ts ├── css │ ├── style.css │ └── tailor.css ├── index.html └── build │ ├── docs.js │ └── docs.js.map ├── tailor.xcf ├── extension ├── icons │ ├── blue │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 128.png │ │ ├── 256.png │ │ └── icon.svg │ └── gray │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 128.png │ │ ├── 256.png │ │ └── icon.svg ├── manifest-chrome.json ├── manifest-ff.json ├── templates │ └── popup.html └── css │ └── popup.css ├── src ├── utils │ └── constants.ts ├── extension │ ├── content.ts │ └── popup.ts └── index.ts ├── .vscode └── settings.json ├── tools ├── hooks │ └── pre-commit ├── copy-hooks.sh ├── build.sh ├── firefox-build.sh ├── chrome-build.sh ├── generate-icons.js └── firefox-run.sh ├── tsconfig-demo.json ├── CHANGELOG.md ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.4.0 2 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailor.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/tailor.xcf -------------------------------------------------------------------------------- /docs/img/ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/docs/img/ff.png -------------------------------------------------------------------------------- /docs/src/docs.ts: -------------------------------------------------------------------------------- 1 | import Tailor from "../../src/index"; 2 | 3 | new Tailor(); 4 | -------------------------------------------------------------------------------- /docs/img/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/docs/img/chrome.png -------------------------------------------------------------------------------- /extension/icons/blue/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/blue/16.png -------------------------------------------------------------------------------- /extension/icons/blue/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/blue/32.png -------------------------------------------------------------------------------- /extension/icons/blue/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/blue/48.png -------------------------------------------------------------------------------- /extension/icons/gray/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/gray/16.png -------------------------------------------------------------------------------- /extension/icons/gray/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/gray/32.png -------------------------------------------------------------------------------- /extension/icons/gray/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/gray/48.png -------------------------------------------------------------------------------- /extension/icons/blue/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/blue/128.png -------------------------------------------------------------------------------- /extension/icons/blue/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/blue/256.png -------------------------------------------------------------------------------- /extension/icons/gray/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/gray/128.png -------------------------------------------------------------------------------- /extension/icons/gray/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/tailor/HEAD/extension/icons/gray/256.png -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const storageKeys = { 2 | ENABLED: "ENABLED", 3 | TOGGLE_KEY: "TOGGLE_KEY", 4 | } as const; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Lint source code 4 | if ! npm run lint:js; then 5 | echo "Please fix errors and commit again" 6 | exit 1 7 | fi 8 | -------------------------------------------------------------------------------- /tsconfig-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "docs", 6 | "target": "ES2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tools/copy-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pre commit hook 4 | cp ./tools/hooks/pre-commit ./.git/hooks/ 5 | chmod +x ./.git/hooks/pre-commit 6 | 7 | echo "Copied git pre-commit hook." 8 | -------------------------------------------------------------------------------- /extension/icons/blue/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /extension/icons/gray/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v0.3.2 4 | 5 | 02.04.2024. 6 | 7 | - Fixed the incorrectly calculated position of an element whose parent was scrolled. 8 | 9 | ### v0.3.1 10 | 11 | - Initial version of the library and extensions. 12 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Clean 4 | rm -rf ./dist 5 | 6 | # Build 7 | ./node_modules/.bin/esbuild ./src/index.ts --minify --sourcemap --outdir=dist 8 | 9 | # Copy CSS to the dist folder 10 | cp ./docs/css/tailor.css ./dist/ 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | dist 4 | .module-cache 5 | *.log* 6 | _nogit 7 | lib 8 | dist-docs 9 | .cache 10 | .parcel-cache 11 | TODO.md 12 | dist 13 | extension/build 14 | web-ext-artifacts 15 | extension/manifest.json 16 | extension/css/tailor.css 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Config helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # We recommend you to keep these unchanged 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | max_line_length = 100 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "declaration": true, 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "outDir": "dist", 9 | "pretty": true, 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "lib": ["dom", "ES2015", "ES2017"], 13 | "module": "ES2020" 14 | }, 15 | "exclude": ["node_modules", "dist"], 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /tools/firefox-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Clean up the build 4 | rm -rf ./extension/build/* 5 | 6 | # Copy Firefox manifest in place 7 | cp ./extension/manifest-ff.json ./extension/manifest.json 8 | 9 | # Copy Tailor CSS 10 | cp ./docs/css/tailor.css ./extension/css/ 11 | 12 | # Build the extension scripts 13 | ./node_modules/.bin/esbuild ./src/extension/*.ts --bundle --minify --sourcemap --outdir=extension/build 14 | 15 | # Bundle the extension 16 | ./node_modules/.bin/web-ext build -s ./extension --overwrite-dest 17 | -------------------------------------------------------------------------------- /tools/chrome-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Clean up the build 4 | rm -rf ./extension/build/* 5 | 6 | # Copy Chrome manifest in place 7 | cp ./extension/manifest-chrome.json ./extension/manifest.json 8 | 9 | # Copy Tailor CSS 10 | cp ./docs/css/tailor.css ./extension/css/ 11 | 12 | # Build the extension scripts 13 | ./node_modules/.bin/esbuild ./src/extension/*.ts --bundle --minify --sourcemap --outdir=extension/build 14 | 15 | # Bundle the extension 16 | zip -r ./web-ext-artifacts/tailor-chrome.zip ./extension/manifest.json ./extension/build/ ./extension/icons/ ./extension/css/ ./extension/templates/ 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "indent": ["error", 2], 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "double"], 17 | "semi": ["error", "always"], 18 | "curly": "error", 19 | "@typescript-eslint/no-explicit-any": "warn" 20 | }, 21 | "ignorePatterns": ["extension", "dist", "docs"] 22 | } 23 | -------------------------------------------------------------------------------- /tools/generate-icons.js: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | // Depends on librsvg 4 | 5 | // brew install librsvg 6 | 7 | const convertSvgToPng = (input, output, height) => { 8 | const command = `rsvg-convert -h ${height} ${input} > ${output}`; 9 | execSync(command); 10 | }; 11 | 12 | const sizes = [16, 32, 48, 128, 256]; 13 | 14 | const generateIcons = (folderPath) => { 15 | const svgPath = `${folderPath}/icon.svg`; 16 | 17 | sizes.forEach((size) => { 18 | convertSvgToPng(svgPath, `${folderPath}/${size}.png`, size); 19 | }); 20 | }; 21 | 22 | const BLUE_PATH = "./extension/icons/blue"; 23 | const GRAY_PATH = "./extension/icons/gray"; 24 | 25 | generateIcons(BLUE_PATH); 26 | generateIcons(GRAY_PATH); 27 | -------------------------------------------------------------------------------- /tools/firefox-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Terminate both web-ext and esbuild processes on Ctrl+C 4 | 5 | terminate() { 6 | pkill -SIGINT -P $$ 7 | exit 8 | } 9 | 10 | trap terminate SIGINT 11 | 12 | # Clean up the build 13 | rm -rf ./extension/build/* 14 | 15 | # Copy Firefox manifest in place 16 | cp ./extension/manifest-ff.json ./extension/manifest.json 17 | 18 | # Copy Tailor CSS 19 | cp ./docs/css/tailor.css ./extension/css/ 20 | 21 | # Run the extension 22 | ./node_modules/.bin/web-ext run -s ./extension --devtools --verbose & 23 | # Store the PID of command2 24 | web_ext_pid=$! 25 | 26 | # Build and watch the extension scripts 27 | ./node_modules/.bin/esbuild ./src/extension/*.ts --watch --bundle --sourcemap --outdir=extension/build 28 | esbuild_pid=$! 29 | 30 | wait 31 | -------------------------------------------------------------------------------- /extension/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Tailor", 4 | "version": "0.3.2", 5 | "author": "Stanko", 6 | "description": "A developer tool which tries to simplify inspecting spacings on websites.", 7 | "homepage_url": "https://muffinman.io/tailor", 8 | "action": { 9 | "default_icon": "icons/blue/256.png", 10 | "default_title": "Tailor", 11 | "default_popup": "templates/popup.html" 12 | }, 13 | "permissions": [ 14 | "storage" 15 | ], 16 | "icons": { 17 | "16": "icons/blue/16.png", 18 | "32": "icons/blue/32.png", 19 | "48": "icons/blue/48.png", 20 | "128": "icons/blue/128.png", 21 | "256": "icons/blue/256.png" 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": [ 26 | "http://*/*", 27 | "https://*/*" 28 | ], 29 | "js": [ 30 | "build/content.js" 31 | ], 32 | "css": [ 33 | "css/tailor.css" 34 | ], 35 | "run_at": "document_end" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stanko 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 | -------------------------------------------------------------------------------- /extension/manifest-ff.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Tailor", 4 | "version": "0.3.2", 5 | "author": "Stanko", 6 | "description": "A developer tool which tries to simplify inspecting spacings on websites.", 7 | "homepage_url": "https://muffinman.io/tailor", 8 | "browser_action": { 9 | "default_icon": "icons/blue/256.png", 10 | "default_title": "Tailor", 11 | "default_popup": "templates/popup.html" 12 | }, 13 | "permissions": [ 14 | "storage" 15 | ], 16 | "icons": { 17 | "16": "icons/blue/16.png", 18 | "32": "icons/blue/32.png", 19 | "48": "icons/blue/48.png", 20 | "128": "icons/blue/128.png", 21 | "256": "icons/blue/256.png" 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": [ 26 | "http://*/*", 27 | "https://*/*" 28 | ], 29 | "js": [ 30 | "build/content.js" 31 | ], 32 | "css": [ 33 | "css/tailor.css" 34 | ], 35 | "run_at": "document_end" 36 | } 37 | ], 38 | "browser_specific_settings": { 39 | "gecko": { 40 | "id": "tailor@muffinman.io", 41 | "strict_min_version": "109.0" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/extension/content.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | import { storageKeys } from "../utils/constants"; 4 | import Tailor from ".."; 5 | 6 | let instance: Tailor; 7 | 8 | const toggle = async (enabled: boolean) => { 9 | const data = await browser.storage.local.get([storageKeys.TOGGLE_KEY]); 10 | 11 | if (enabled) { 12 | instance = new Tailor(data[storageKeys.TOGGLE_KEY]); 13 | } else { 14 | instance?.destroy(); 15 | } 16 | }; 17 | 18 | browser.storage.local.onChanged.addListener((changes) => { 19 | const changedItems = Object.keys(changes); 20 | 21 | changedItems.forEach((item) => { 22 | if (item === storageKeys.TOGGLE_KEY && instance) { 23 | instance.updateToggleKey(changes[item].newValue); 24 | } else if (item === storageKeys.ENABLED) { 25 | toggle(changes[item].newValue); 26 | } 27 | }); 28 | }); 29 | 30 | const init = async () => { 31 | const data = await browser.storage.local.get([storageKeys.ENABLED]); 32 | 33 | // Tailor is enabled by default 34 | // Only disable it if the value is explicitly set to false 35 | const enabled = data[storageKeys.ENABLED] !== false; 36 | 37 | toggle(enabled); 38 | }; 39 | 40 | init(); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stanko/tailor", 3 | "version": "0.3.2", 4 | "private": false, 5 | "license": "MIT", 6 | "description": "", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "start": "esbuild docs/src/docs.ts --bundle --tsconfig=tsconfig-demo.json --servedir=docs --outdir=docs/build", 12 | "build": "sh ./tools/build.sh", 13 | "build:docs": "esbuild docs/src/docs.ts --bundle --tsconfig=tsconfig-demo.json --outdir=docs/build --minify --sourcemap", 14 | "firefox:run": "sh ./tools/firefox-run.sh", 15 | "firefox:build": "sh ./tools/firefox-build.sh", 16 | "chrome:build": "sh ./tools/chrome-build.sh", 17 | "lint:js": "eslint .", 18 | "generate-icons": "node ./tools/generate-icons.js", 19 | "prepublishOnly": "npm run build" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^7.4.0", 23 | "@typescript-eslint/parser": "^7.4.0", 24 | "chrome-types": "^0.1.275", 25 | "esbuild": "^0.20.2", 26 | "eslint": "^8.57.0", 27 | "typescript": "^5.4.3", 28 | "web-ext": "^7.11.0", 29 | "webextension-polyfill": "^0.10.0" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+ssh://git@github.com/Stanko/tailor.git" 34 | }, 35 | "keywords": [], 36 | "author": "Stanko", 37 | "bugs": { 38 | "url": "https://github.com/Stanko/tailor/issues" 39 | }, 40 | "homepage": "https://github.com/Stanko/tailor#readme", 41 | "files": [ 42 | "dist/", 43 | "CHANGELOG.md" 44 | ], 45 | "dependencies": { 46 | "@types/webextension-polyfill": "^0.10.7" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/extension/popup.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | import { storageKeys } from "../utils/constants"; 4 | 5 | const setIcon = (color: "blue" | "gray") => { 6 | // For Firefox Tailor is using manifest v2 therefore it uses browser.browserAction 7 | const action = browser.action || browser.browserAction; 8 | 9 | action.setIcon({ 10 | path: { 11 | "16": `/icons/${color}/16.png`, 12 | "32": `/icons/${color}/32.png`, 13 | "48": `/icons/${color}/48.png`, 14 | "128": `/icons/${color}/128.png`, 15 | "256": `/icons/${color}/256.png`, 16 | }, 17 | }); 18 | }; 19 | 20 | const $enabledCheckbox = document.querySelector("input[name=enabled]") as HTMLInputElement; 21 | const $activationKeyRadios = document.querySelectorAll( 22 | "input[name=toggle-key]" 23 | ) as NodeListOf; 24 | 25 | browser.storage.local.get([storageKeys.ENABLED, storageKeys.TOGGLE_KEY]).then((res) => { 26 | // Tailor is enabled by default 27 | // Only disable it if the value is explicitly set to false 28 | const initEnabled = res[storageKeys.ENABLED] !== false; 29 | 30 | $enabledCheckbox.checked = initEnabled; 31 | 32 | if (initEnabled) { 33 | setIcon("blue"); 34 | } else { 35 | setIcon("gray"); 36 | } 37 | 38 | $enabledCheckbox.addEventListener("change", () => { 39 | browser.storage.local.set({ 40 | [storageKeys.ENABLED]: $enabledCheckbox.checked, 41 | }); 42 | if ($enabledCheckbox.checked) { 43 | setIcon("blue"); 44 | } else { 45 | setIcon("gray"); 46 | } 47 | }); 48 | 49 | // Toggle key 50 | const initToggleKey = res[storageKeys.TOGGLE_KEY] || "Alt"; 51 | 52 | $activationKeyRadios.forEach(($radio) => { 53 | $radio.checked = initToggleKey === $radio.value; 54 | 55 | $radio.addEventListener("change", () => { 56 | browser.storage.local.set({ 57 | [storageKeys.TOGGLE_KEY]: $radio.value, 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /extension/templates/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailor Popup 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 23 |

24 | Tailor is enabled 25 |

26 |

27 | Tailor is disabled 28 |

29 |
30 | 31 |
32 |

Select toggle key

33 |
34 | 35 | 39 | 40 | 45 | 46 | 51 |
52 |

53 | While holding this key, Tailor is active, allowing you to inspect elements on the page. 54 |

55 |
56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailor 2 | 3 | Tailor is a developer tool that simplifies inspecting spacings on websites. While holding the toggle key (Alt or option), hover around the page to inspect elements. Size, margin, and padding will be highlighted, and font information will be displayed in the panel. 4 | 5 | While Tailor is active, you can also click on an element to lock it and enter the measuring mode. Now, Tailor will display the distance between the locked element and any other element you hover over. 6 | 7 | Try it yourself on the demo page - https://muffinman.io/tailor/ 8 | 9 | Desktop only. 10 | 11 | Our QA team does extensive checks to ensure that spacings, paddings, and font sizes match the designs. I created this library/extension hoping to make their lives easier. 12 | 13 | ## Browser Extensions 14 | 15 | 16 | 17 | 18 | 19 | ## Library 20 | 21 | You can also use Tailor directly in your projects. Install the package: 22 | 23 | ``` 24 | npm install @stanko/tailor 25 | ``` 26 | 27 | Load the CSS and instantiate it: 28 | 29 | ```js 30 | import Tailor from "@stanko/tailor"; 31 | import "@stanko/tailor/dist/tailor.css"; 32 | 33 | new Tailor(); 34 | ``` 35 | 36 | I would suggest disabling it in production, something like: 37 | 38 | ```js 39 | if (NODE_ENV !== "production") { 40 | new Tailor(); 41 | } 42 | ``` 43 | 44 | Tailor is only available as an ESM module. 45 | 46 | ## Bookmarklet 47 | 48 | You can also load Tailor using this bookmarklet. However, please be aware that depending on a site's security policies, it may not load. 49 | 50 | Please drop the following code into the console or bookmark: 51 | 52 | ```js 53 | javascript: void (async function () { 54 | const Tailor = await import("https://esm.sh/@stanko/tailor"); 55 | new Tailor.default(); 56 | const style = document.createElement("link"); 57 | style.setAttribute("href", "https://esm.sh/@stanko/tailor/dist/tailor.css"); 58 | style.setAttribute("rel", "stylesheet"); 59 | document.head.append(style); 60 | })(); 61 | ``` 62 | 63 | ## Setup 64 | 65 | - Use node version from `.nvmrc` - `nvm use` 66 | - Install dependencies - `npm install` 67 | - Copy git hooks - `sh ./tools/copy-hooks.sh` 68 | 69 | ## Development server 70 | 71 | - Start a simple development - `npm start` 72 | - Open http://localhost:8000/ 73 | 74 | ## Developing extensions 75 | 76 | ### Firefox 77 | 78 | Project uses [web-ext](https://github.com/mozilla/web-ext) for Firefox extension development. 79 | 80 | - Start extension in development mode - `npm run firefox:run` 81 | - Build Firefox extension - `npm run firefox:build` 82 | 83 | ### Chrome 84 | 85 | - For now, copy `manifest-chrome.json` to `manifest.json` and load the unpacked extension 86 | - Build Chrome extension - `npm run chrome:build` 87 | 88 | ## TODO 89 | 90 | - [ ] Info panel improvements 91 | - [ ] Handle touch events when simulating phones/tablets in devtools 92 | -------------------------------------------------------------------------------- /extension/css/popup.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | padding: 0; 5 | margin: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | :root { 10 | --bg: #f9fafb; 11 | --fg: #030712; 12 | 13 | --blue: #0b99ff; 14 | --blue-100: rgba(11, 153, 255, 0.1); 15 | --blue-300: rgba(11, 153, 255, 0.3); 16 | 17 | --gray-50: #f9fafb; 18 | --gray-100: #f3f4f6; 19 | --gray-200: #e5e7eb; 20 | --gray-300: #d1d5db; 21 | --gray-400: #9ca3af; 22 | --gray-500: #6b7280; 23 | } 24 | 25 | @media (prefers-color-scheme: dark) { 26 | :root { 27 | --bg: #111827; 28 | --fg: #f3f4f6; 29 | 30 | --blue: #0b99ff; 31 | --blue-100: rgba(11, 153, 255, 0.2); 32 | --blue-300: rgba(11, 153, 255, 0.4); 33 | 34 | --gray-500: #9ca3af; 35 | --gray-400: #6b7280; 36 | --gray-300: #4b5563; 37 | --gray-200: #374151; 38 | --gray-100: #1f2937; 39 | --gray-50: #182431; 40 | } 41 | } 42 | 43 | body { 44 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, 45 | Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 46 | padding: 1rem; 47 | width: 18rem; 48 | font-size: 0.875rem; 49 | background-color: var(--bg); 50 | color: var(--fg); 51 | } 52 | 53 | h1 { 54 | font-size: 1rem; 55 | font-weight: 500; 56 | } 57 | 58 | h2 { 59 | font-size: 0.875rem; 60 | font-weight: 500; 61 | } 62 | 63 | p { 64 | color: var(--gray-500); 65 | } 66 | 67 | label:active { 68 | transform: translateY(1px); 69 | cursor: pointer; 70 | } 71 | 72 | /* Enabled */ 73 | 74 | .enabled-wrapper { 75 | display: flex; 76 | gap: 0.75rem; 77 | align-items: center; 78 | flex-direction: column; 79 | } 80 | 81 | .enabled-wrapper input { 82 | position: absolute; 83 | opacity: 0; 84 | } 85 | 86 | .power-button { 87 | --power-button-stroke: var(--gray-200); 88 | --power-button-bg: var(--gray-100); 89 | --power-button-fg: var(--gray-300); 90 | } 91 | 92 | .enabled-wrapper input:checked + .power-button { 93 | --power-button-stroke: var(--blue-300); 94 | --power-button-bg: var(--blue-100); 95 | --power-button-fg: var(--blue); 96 | } 97 | 98 | .is-enabled, 99 | .is-disabled { 100 | order: -1; 101 | display: none; 102 | } 103 | 104 | .enabled-wrapper input:checked + .power-button + .is-enabled, 105 | .enabled-wrapper input:not(:checked) + .power-button + .is-enabled + .is-disabled { 106 | display: block; 107 | } 108 | 109 | .power-button svg { 110 | width: 8rem; 111 | height: 8rem; 112 | overflow: visible; 113 | } 114 | 115 | /* Toggle key */ 116 | 117 | .toggle-key { 118 | display: flex; 119 | flex-direction: column; 120 | gap: 0.75rem; 121 | margin-top: 1rem; 122 | } 123 | 124 | .toggle-key input { 125 | position: absolute; 126 | opacity: 0; 127 | } 128 | 129 | .keyboard-buttons { 130 | display: flex; 131 | gap: 0.5rem; 132 | } 133 | 134 | .keyboard-button { 135 | position: relative; 136 | display: flex; 137 | flex-direction: column; 138 | border-radius: 0.5rem; 139 | border: 0.125rem solid var(--gray-300); 140 | padding: 0.5rem; 141 | min-height: 4.5rem; 142 | justify-content: space-between; 143 | background: var(--gray-100); 144 | color: var(--gray-500); 145 | flex: 1; 146 | } 147 | 148 | .toggle-key input:checked + .keyboard-button { 149 | border-color: var(--blue); 150 | background-color: var(--blue-100); 151 | color: var(--fg); 152 | } 153 | 154 | .keyboard-button--control { 155 | flex-direction: column-reverse; 156 | } 157 | 158 | .keyboard-button-symbol { 159 | position: absolute; 160 | top: 0.5rem; 161 | right: 0.5rem; 162 | } 163 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | /* ---------- Reset ---------- */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | * { 9 | margin: 0; 10 | } 11 | 12 | html:focus-within { 13 | scroll-behavior: smooth; 14 | } 15 | 16 | @media (prefers-reduced-motion: reduce) { 17 | html:focus-within { 18 | scroll-behavior: auto; 19 | } 20 | 21 | *, 22 | *::before, 23 | *::after { 24 | transition: none !important; 25 | animation: none !important; 26 | scroll-behavior: auto !important; 27 | } 28 | } 29 | 30 | body { 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, 34 | Inter, Arial, "Noto Sans", sans-serif; 35 | line-height: 1.4; 36 | font-size: 1.125rem; 37 | } 38 | 39 | img, 40 | picture, 41 | video, 42 | canvas, 43 | svg { 44 | display: block; 45 | max-width: 100%; 46 | } 47 | 48 | input, 49 | button, 50 | textarea, 51 | select { 52 | font: inherit; 53 | color: inherit; 54 | } 55 | 56 | input { 57 | border-radius: 0; 58 | } 59 | 60 | p, 61 | h1, 62 | h2, 63 | h3, 64 | h4, 65 | h5, 66 | h6 { 67 | overflow-wrap: break-word; 68 | } 69 | 70 | button { 71 | cursor: pointer; 72 | } 73 | 74 | table { 75 | border-collapse: collapse; 76 | } 77 | 78 | /* ---------- Styles ---------- */ 79 | 80 | body { 81 | padding: 2rem; 82 | color: #111; 83 | background: #fafafe; 84 | } 85 | 86 | @media (prefers-color-scheme: dark) { 87 | body { 88 | background: #1e1e1f; 89 | color: #f7f7fa; 90 | } 91 | } 92 | 93 | @media (min-width: 600px) { 94 | body { 95 | padding: 3rem; 96 | } 97 | } 98 | 99 | h1 { 100 | margin-bottom: 1.5rem; 101 | display: inline-flex; 102 | gap: 0.75rem; 103 | align-items: center; 104 | } 105 | 106 | h2 { 107 | margin-block: 3rem 1.5rem; 108 | font-weight: 500; 109 | } 110 | 111 | h1 svg { 112 | width: 3rem; 113 | height: 3rem; 114 | } 115 | 116 | p { 117 | margin-block: 1rem; 118 | } 119 | 120 | main { 121 | max-width: 30rem; 122 | margin: 0 auto; 123 | } 124 | 125 | code { 126 | background-color: #f1f1f1; 127 | border-radius: 0.5rem; 128 | border: 1px solid #ddd; 129 | color: #349; 130 | display: inline-block; 131 | margin-block: -0.05rem; 132 | padding: 0.05rem 0.5rem; 133 | } 134 | 135 | button { 136 | color: #111; 137 | } 138 | 139 | a { 140 | color: var(--tailor-blue); 141 | } 142 | 143 | a:hover { 144 | color: inherit; 145 | } 146 | 147 | .extensions { 148 | display: flex; 149 | flex-wrap: wrap; 150 | gap: 1rem; 151 | } 152 | 153 | .extensions img { 154 | height: 4rem; 155 | flex: 1; 156 | } 157 | 158 | .dummy { 159 | display: flex; 160 | align-items: center; 161 | gap: 1rem; 162 | margin-bottom: 2rem; 163 | } 164 | 165 | .playground { 166 | position: relative; 167 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande", "Lucida Sans", Arial, 168 | sans-serif; 169 | max-width: 580px; 170 | margin: 0 auto; 171 | } 172 | 173 | .playground-row { 174 | display: flex; 175 | font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif; 176 | margin-inline: -10px; 177 | } 178 | 179 | .playground-item { 180 | flex: 120px 0 1; 181 | aspect-ratio: 1; 182 | border: 1px solid #ddd; 183 | position: relative; 184 | z-index: 1; 185 | margin: 10px; 186 | padding: 10px; 187 | font-family: "Times New Roman", Times, serif; 188 | } 189 | 190 | .playground-item--mid { 191 | position: absolute; 192 | top: 25%; 193 | left: 25%; 194 | width: 50%; 195 | height: 50%; 196 | z-index: 0; 197 | font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif; 198 | } 199 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | Tailor 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |

34 | 35 | 36 | 37 | 38 | 39 | Tailor 40 |

41 |

42 | Tailor is a developer tool which tries to simplify inspecting spacings on websites. 43 | Test it out on this page, hold alt (or option on mac) 44 | and hover around the page to start inspecting elements. 45 |

46 |

47 | Desktop only. 48 |

49 |

50 | Check the documentation and source code on GitHub 51 |

52 | 53 |

Browser extensions

54 | 55 |
56 | 57 | 58 | 59 | 60 |
61 | 62 |

Dummy elements

63 | 64 |
65 | 66 | Link 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 |
77 |
78 | 79 | 80 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/css/tailor.css: -------------------------------------------------------------------------------- 1 | /* ---------- Tailor ---------- */ 2 | 3 | :root { 4 | --tailor-red: #f14722; 5 | --tailor-purple: #953edd; 6 | --tailor-blue: #0b99ff; 7 | --tailor-green: #00bb70; 8 | --tailor-light-blue: #46c4ff; 9 | --tailor-blue-500: rgba(11, 153, 255, 0.5); 10 | --tailor-blue-50: rgba(11, 153, 255, 0.05); 11 | --tailor-red-50: rgba(241, 71, 34, 0.05); 12 | --tailor-panel-black: rgba(0, 0, 0, 0.7); 13 | --tailor-white: #fff; 14 | } 15 | 16 | .__tailor, 17 | .__tailor * { 18 | padding: 0; 19 | margin: 0; 20 | box-sizing: border-box; 21 | } 22 | 23 | .__tailor { 24 | display: none; 25 | pointer-events: none; 26 | z-index: 99000; 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | overflow: hidden; 33 | font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, 34 | Inter, Arial, "Noto Sans", sans-serif; 35 | } 36 | 37 | .__tailor--measuring .__tailor-highlight { 38 | background-color: var(--tailor-blue-50); 39 | } 40 | 41 | .__tailor--measuring .__tailor-padding, 42 | .__tailor--measuring .__tailor-margin { 43 | display: none; 44 | } 45 | 46 | .__tailor--measuring .__tailor-ruler, 47 | .__tailor--measuring .__tailor-ruler-helper { 48 | display: block; 49 | } 50 | 51 | .__tailor-mask { 52 | position: absolute; 53 | } 54 | 55 | .__tailor-highlight { 56 | position: absolute; 57 | } 58 | 59 | /* Using a pseudo element so it can be positioned above the padding and margin elements */ 60 | .__tailor-highlight::before { 61 | content: ""; 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | z-index: 10001; 68 | outline: 1px solid var(--tailor-blue); 69 | outline-offset: -1px; 70 | } 71 | 72 | /* ----- Guides ----- */ 73 | 74 | .__tailor-mask::before, 75 | .__tailor-mask::after { 76 | content: ""; 77 | position: absolute; 78 | outline: 1px dotted var(--tailor-blue-500); 79 | outline-offset: -1px; 80 | } 81 | 82 | .__tailor-mask::before { 83 | height: 250vh; 84 | width: 100%; 85 | left: 0; 86 | top: -100vh; 87 | } 88 | 89 | .__tailor-mask::after { 90 | width: 250vw; 91 | height: 100%; 92 | top: 0; 93 | left: -100vw; 94 | } 95 | 96 | /* ----- Padding ----- */ 97 | 98 | .__tailor-padding { 99 | width: 100%; 100 | height: 100%; 101 | position: relative; 102 | outline: 1px dotted var(--tailor-green); 103 | outline-offset: -1px; 104 | } 105 | 106 | /* ----- Padding labels ----- */ 107 | 108 | .__tailor-padding-label { 109 | background-color: var(--tailor-green); 110 | color: white; 111 | padding: 1px 3px; 112 | border-radius: 3px; 113 | font-size: 11px; 114 | font-weight: bold; 115 | display: inline-block; 116 | position: absolute; 117 | } 118 | 119 | .__tailor-padding-label--bottom, 120 | .__tailor-padding-label--top { 121 | left: 50%; 122 | transform: translateX(-50%); 123 | } 124 | 125 | .__tailor-padding-label--top { 126 | bottom: 100%; 127 | } 128 | 129 | .__tailor-padding-label--bottom { 130 | top: 100%; 131 | } 132 | 133 | .__tailor-padding-label--left, 134 | .__tailor-padding-label--right { 135 | top: 50%; 136 | transform: translateY(-50%); 137 | } 138 | 139 | .__tailor-padding-label--right { 140 | left: 100%; 141 | } 142 | 143 | .__tailor-padding-label--left { 144 | right: 100%; 145 | } 146 | 147 | /* ----- Margin ----- */ 148 | 149 | .__tailor-margin { 150 | position: absolute; 151 | outline: 1px dotted var(--tailor-purple); 152 | outline-offset: -1px; 153 | } 154 | 155 | /* ----- Margin labels ----- */ 156 | 157 | .__tailor-margin-label { 158 | background-color: var(--tailor-purple); 159 | color: white; 160 | padding: 1px 3px; 161 | border-radius: 3px; 162 | font-size: 11px; 163 | font-weight: bold; 164 | display: inline-block; 165 | position: absolute; 166 | } 167 | 168 | .__tailor-margin-label--bottom, 169 | .__tailor-margin-label--top { 170 | left: 50%; 171 | transform: translateX(-50%); 172 | } 173 | 174 | .__tailor-margin-label--top { 175 | bottom: 100%; 176 | } 177 | 178 | .__tailor-margin-label--bottom { 179 | top: 100%; 180 | } 181 | 182 | .__tailor-margin-label--left, 183 | .__tailor-margin-label--right { 184 | top: 50%; 185 | transform: translateY(-50%); 186 | } 187 | 188 | .__tailor-margin-label--right { 189 | left: 100%; 190 | } 191 | 192 | .__tailor-margin-label--left { 193 | right: 100%; 194 | } 195 | 196 | /* ----- Measures ----- */ 197 | 198 | .__tailor-to-mask { 199 | position: absolute; 200 | outline: 1px solid var(--tailor-red); 201 | outline-offset: -1px; 202 | background-color: var(--tailor-red-50); 203 | } 204 | 205 | .__tailor-ruler { 206 | display: none; 207 | position: absolute; 208 | border: 1px solid var(--tailor-red); 209 | } 210 | 211 | .__tailor-ruler div { 212 | color: white; 213 | background: var(--tailor-red); 214 | border-radius: 3px; 215 | padding: 3px 5px; 216 | font-size: 12px; 217 | font-weight: bold; 218 | display: inline-block; 219 | position: absolute; 220 | z-index: 10001; 221 | } 222 | 223 | .__tailor-ruler div:empty { 224 | display: none; 225 | } 226 | 227 | .__tailor-ruler--x div { 228 | transform: translate(-50%, -50%); 229 | left: 50%; 230 | } 231 | 232 | .__tailor-ruler--y div { 233 | transform: translate(-50%, -50%); 234 | top: 50%; 235 | } 236 | 237 | .__tailor-ruler-helper { 238 | display: none; 239 | position: absolute; 240 | border: 1px dotted var(--tailor-red); 241 | } 242 | 243 | .__tailor-ruler--x, 244 | .__tailor-ruler-helper--x { 245 | border-width: 1px 0 0 0; 246 | } 247 | 248 | .__tailor-ruler--y, 249 | .__tailor-ruler-helper--y { 250 | border-width: 0 1px 0 0; 251 | } 252 | 253 | /* ----- Panel ----- */ 254 | 255 | .__tailor-panel { 256 | position: fixed; 257 | overflow: hidden; 258 | top: 10px; 259 | right: 10px; 260 | background-color: var(--tailor-panel-black); 261 | backdrop-filter: blur(3px); 262 | border-radius: 8px; 263 | padding: 10px; 264 | color: var(--tailor-white); 265 | width: 200px; 266 | font-size: 13px; 267 | text-overflow: ellipsis; 268 | white-space: nowrap; 269 | } 270 | 271 | .__tailor-panel span { 272 | color: var(--tailor-light-blue); 273 | font-weight: bold; 274 | } 275 | -------------------------------------------------------------------------------- /docs/build/docs.js: -------------------------------------------------------------------------------- 1 | "use strict";(()=>{function k(r,e={},t=[]){let i=document.createElement(r);Array.isArray(t)||(t=[t]),t.forEach(l=>{typeof l=="string"?i.append(l):i.appendChild(l)});for(let l in e)i.setAttribute(l,e[l]);return i}var o=(r={},e=[])=>k("div",r,e),D=(r={},e=[])=>k("span",r,e);function f(r){let e=r.getBoundingClientRect();return e.x+=window.scrollX,e.y+=window.scrollY,e}function F(r){if(r instanceof SVGElement)return f(r);let e=0,t=0,i=r,l=r.offsetWidth,s=r.offsetHeight;do e+=i.offsetTop||0,t+=i.offsetLeft||0,e-=i.scrollTop||0,t-=i.scrollLeft||0,i=i.offsetParent;while(i);return{x:t,y:e,width:l,height:s}}function w(r,e=1){return+r.toFixed(e)}function m(r,e,t,i,l){r.style.left=`${e}px`,r.style.top=`${t}px`,r.style.width=`${i}px`,r.style.height=`${l}px`}function H(r,e,t,i,l=0){if(e>i){let a=i;i=e,e=a}let s=w(i-e);r.firstChild&&(r.firstChild.textContent=s>0?s.toString():""),m(r,e,t-l,s,0)}function E(r,e,t,i,l=0){if(t>i){let a=i;i=t,t=a}let s=w(i-t);r.firstChild&&(r.firstChild.textContent=s>0?s.toString():""),m(r,e-l,t,0,s)}var T=class{constructor(e="Alt"){this.$current=null;this.$measureTo=null;this.selected=!1;this.handleContextMenu=e=>{e.preventDefault()};this.handleKeyDown=e=>{e.repeat||e.key===this.toggleKey&&this.enable()};this.handleKeyUp=e=>{e.key===this.toggleKey&&this.disable()};this.handleWindowBlur=()=>{this.disable()};this.handleMouseMove=e=>{let t=e.target;this.selected?this.$measureTo!==t&&this.$current&&this.$current!==t&&(this.measureDistance(this.$current,t),this.updatePanel(t)):this.$current!==e.target&&(this.$current=t,this.highlightElement(this.$current),this.updatePanel(this.$current))};this.handleClick=e=>{e.preventDefault(),e.stopPropagation()};this.handleMouseDown=e=>{e.preventDefault(),e.stopPropagation(),this.selected=!0,this.$tailor.classList.add("__tailor--measuring"),this.$current=e.target,this.highlightElement(this.$current),this.$toMask.setAttribute("style","")};if(this.toggleKey=e,this.$mask=o({class:"__tailor-mask"}),this.$toMask=o({class:"__tailor-to-mask"}),this.$margin=o({class:"__tailor-margin"}),this.$padding=o({class:"__tailor-padding"}),this.$highlight=o({class:"__tailor-highlight"},[this.$margin,this.$padding]),this.$xRuler=o({class:"__tailor-ruler __tailor-ruler--x"},o()),this.$xRuler2=o({class:"__tailor-ruler __tailor-ruler--x"},o()),this.$yRuler=o({class:"__tailor-ruler __tailor-ruler--y"},o()),this.$yRuler2=o({class:"__tailor-ruler __tailor-ruler--y"},o()),this.$xRulerHelper=o({class:"__tailor-ruler-helper __tailor-ruler-helper--x"}),this.$xRulerHelper2=o({class:"__tailor-ruler-helper __tailor-ruler-helper--x"}),this.$yRulerHelper=o({class:"__tailor-ruler-helper __tailor-ruler-helper--y"}),this.$yRulerHelper2=o({class:"__tailor-ruler-helper __tailor-ruler-helper--y"}),this.$panel=o({class:"__tailor-panel"}),this.$rulers=[this.$xRuler,this.$xRuler2,this.$yRuler,this.$yRuler2],this.$helpers=[this.$xRulerHelper,this.$xRulerHelper2,this.$yRulerHelper,this.$yRulerHelper2],this.$tailor=o({class:"__tailor"},[this.$mask,this.$highlight,this.$toMask,...this.$rulers,...this.$helpers,this.$panel]),this.$elementsToReset=[this.$mask,this.$highlight,this.$margin,this.$toMask],window.__tailor_instance)return window.__tailor_instance;document.body.append(this.$tailor),window.addEventListener("keydown",this.handleKeyDown),window.addEventListener("keyup",this.handleKeyUp),window.addEventListener("blur",this.handleWindowBlur),window.__tailor_instance=this,console.log("%c T ","background-color: #0b99ff; color: white; line-height: 17px; display: inline-block;","Tailor initialized")}updateToggleKey(e){this.toggleKey=e}enable(){this.$panel.replaceChildren(D({},["Tailor"])," ready"),this.$tailor.style.display="block",this.$tailor.style.width=document.body.scrollWidth+"px",this.$tailor.style.height=document.body.scrollHeight+"px",window.addEventListener("mousedown",this.handleMouseDown,!0),window.addEventListener("click",this.handleClick,!0),window.addEventListener("mousemove",this.handleMouseMove),window.addEventListener("contextmenu",this.handleContextMenu)}disable(){window.removeEventListener("mousedown",this.handleMouseDown,!0),window.removeEventListener("click",this.handleClick,!0),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("contextmenu",this.handleContextMenu),this.$elementsToReset.forEach(e=>{e.setAttribute("style","")}),this.$margin.textContent="",this.$padding.textContent="",this.resetRulers(),this.$tailor.style.display="none",this.$current=null,this.selected=!1,this.$tailor.classList.remove("__tailor--measuring"),this.$measureTo=null}resetRulers(){this.$rulers.forEach(e=>{e.setAttribute("style",""),e.firstChild.textContent=""}),this.$helpers.forEach(e=>{e.setAttribute("style","")})}destroy(){this.disable(),this.$tailor.remove(),delete window.__tailor_instance,window.removeEventListener("keyup",this.handleKeyUp),window.removeEventListener("keydown",this.handleKeyDown),window.removeEventListener("blur",this.handleWindowBlur)}highlightElement(e){this.resetRulers();let t=getComputedStyle(e),i=f(e),l=this.selected?i:F(e),{$mask:s,$highlight:a,$margin:u,$padding:c}=this,h={top:parseFloat(t.marginTop),bottom:parseFloat(t.marginBottom),left:parseFloat(t.marginLeft),right:parseFloat(t.marginRight),inline:parseFloat(t.marginLeft)+parseFloat(t.marginRight),block:parseFloat(t.marginTop)+parseFloat(t.marginBottom)},y={top:parseFloat(t.paddingTop),bottom:parseFloat(t.paddingBottom),left:parseFloat(t.paddingLeft),right:parseFloat(t.paddingRight),inline:parseFloat(t.paddingLeft)+parseFloat(t.paddingRight),block:parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)};m(a,l.x,l.y,l.width,l.height),m(s,i.x,i.y,i.width,i.height),a.style.paddingLeft=t.paddingLeft,a.style.paddingRight=t.paddingRight,a.style.paddingTop=t.paddingTop,a.style.paddingBottom=t.paddingBottom,a.style.transform=this.selected?"":t.transform,m(u,-h.left,-h.top,l.width+h.inline,l.height+h.block);let $=[],v=[];["top","right","bottom","left"].forEach(p=>{h[p]&&$.push(o({class:`__tailor-margin-label __tailor-margin-label--${p}`},[w(h[p]).toString()])),y[p]&&v.push(o({class:`__tailor-padding-label __tailor-padding-label--${p}`},[w(y[p]).toString()]))}),u.replaceChildren(...$),c.replaceChildren(...v)}measureDistance(e,t){let i=f(e),l=f(t),{$xRuler:s,$xRuler2:a,$yRuler:u,$yRuler2:c,$xRulerHelper:h,$xRulerHelper2:y,$yRulerHelper:$,$yRulerHelper2:v}=this;t&&m(this.$toMask,l.left,l.top,l.width,l.height);let n={horizontal1:[0,0,0,0],horizontal2:[0,0,0,0],vertical1:[0,0,0,0],vertical2:[0,0,0,0],hHelper1:[0,0,0,0],hHelper2:[0,0,0,0],vHelper1:[0,0,0,0],vHelper2:[0,0,0,0]};this.$measureTo=t;let p=i.bottom<=l.top,_=i.top>=l.bottom,x=!p&&!_,L=i.right<=l.left,M=i.left>=l.right,R=!L&&!M,d={x:i.left+i.width*.5,y:i.top+i.height*.5};if(R){let g=Math.max(i.left,l.left),b=Math.min(i.right,l.right);d.x=g+(b-g)/2}if(x){let g=Math.max(i.top,l.top),b=Math.min(i.bottom,l.bottom);d.y=g+(b-g)/2}p?n.vertical1=[d.x,i.bottom,l.top,0]:_?n.vertical1=[d.x,i.top,l.bottom,1]:(n.vertical1=[d.x,i.bottom,l.bottom,1],n.vertical2=[d.x,i.top,l.top,0]),L?n.horizontal1=[i.right,d.y,l.left,0]:M?n.horizontal1=[i.left,d.y,l.right,1]:(n.horizontal1=[i.right,d.y,l.right,1],n.horizontal2=[i.left,d.y,l.left,0]),(L||M)&&(n.hHelper1=[d.x,n.vertical1[2],l.left,n.vertical1[3]],x&&(n.hHelper2=[d.x,n.vertical2[2],l.left,0])),(p||_)&&(n.vHelper1=[n.horizontal1[2],d.y,l.top,n.horizontal1[3]],R&&(n.vHelper2=[n.horizontal2[2],d.y,l.top,0])),E(u,...n.vertical1),E(c,...n.vertical2),H(s,...n.horizontal1),H(a,...n.horizontal2),H(h,...n.hHelper1),H(y,...n.hHelper2),E($,...n.vHelper1),E(v,...n.vHelper2)}updatePanel(e){let t=getComputedStyle(e),i=e.id?`#${e.id}`:"",l=e.getAttribute("class");l?l=`.${e.getAttribute("class")?.split(" ").join(".")}`:l="";let s=t.fontFamily,a=t.fontFamily.split(", ");for(let h in a)if(document.fonts.check(`16px ${a[h]}`)){s=a[h].replace(/"/g,"");break}let u=t.height.replace("px",""),c=t.width.replace("px","");if(u==="auto"||c==="auto")if(e instanceof SVGElement){let h=f(e);u=h.height.toString(),c=h.width.toString()}else u=e.offsetHeight.toString(),c=e.offsetWidth.toString();this.$panel.replaceChildren(D({},[e.tagName.toLowerCase()]),`${i}${l}`,o({},[`${c}x${u}px`]),o({},[s]),o({},[`${t.fontSize} ${t.lineHeight}`]),o({},[`${t.fontWeight} ${t.fontStyle}`]))}},C=T;new C;})(); 2 | //# sourceMappingURL=docs.js.map 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | type Vector = { x: number; y: number }; 2 | 3 | type Child = string | HTMLElement | SVGElement; 4 | 5 | function el( 6 | tagName: string, 7 | attributes: Record = {}, 8 | children: Child | Child[] = [] 9 | ): HTMLElement { 10 | // Create element 11 | const $div = document.createElement(tagName); 12 | 13 | // If children is a single element, wrap it into array 14 | if (!Array.isArray(children)) { 15 | children = [children]; 16 | } 17 | 18 | // Loop through and append children 19 | children.forEach((child) => { 20 | if (typeof child === "string") { 21 | $div.append(child); 22 | } else { 23 | $div.appendChild(child); 24 | } 25 | }); 26 | 27 | // Sett HTML attributes 28 | for (const name in attributes) { 29 | $div.setAttribute(name, attributes[name]); 30 | } 31 | 32 | return $div; 33 | } 34 | 35 | const div = (attributes: Record = {}, children: Child | Child[] = []) => { 36 | return el("div", attributes, children) as HTMLDivElement; 37 | }; 38 | 39 | const span = (attributes: Record = {}, children: Child | Child[] = []) => { 40 | return el("span", attributes, children) as HTMLSpanElement; 41 | }; 42 | 43 | type ToggleKey = "Control" | "Meta" | "Alt"; 44 | 45 | function getRect($el: HTMLElement | SVGElement) { 46 | const rect = $el.getBoundingClientRect(); 47 | rect.x += window.scrollX; 48 | rect.y += window.scrollY; 49 | return rect; 50 | } 51 | 52 | function getBox($el: HTMLElement | SVGElement) { 53 | if ($el instanceof SVGElement) { 54 | return getRect($el); 55 | } 56 | 57 | let y = 0; 58 | let x = 0; 59 | let $element: HTMLElement | null = $el; 60 | const width = $el.offsetWidth; 61 | const height = $el.offsetHeight; 62 | 63 | do { 64 | y += $element.offsetTop || 0; 65 | x += $element.offsetLeft || 0; 66 | y -= $element.scrollTop || 0; 67 | x -= $element.scrollLeft || 0; 68 | // TODO check if this is safe to cast 69 | $element = $element.offsetParent as HTMLElement; 70 | } while ($element); 71 | 72 | return { 73 | x, 74 | y, 75 | width, 76 | height, 77 | }; 78 | } 79 | 80 | function toFixed(n: number, decimals: number = 1) { 81 | return +n.toFixed(decimals); 82 | } 83 | 84 | function setPosition($el: HTMLElement, left: number, top: number, width: number, height: number) { 85 | $el.style.left = `${left}px`; 86 | $el.style.top = `${top}px`; 87 | $el.style.width = `${width}px`; 88 | $el.style.height = `${height}px`; 89 | } 90 | 91 | function setHorizontalRuler( 92 | $el: HTMLDivElement, 93 | xStart: number, 94 | yStart: number, 95 | xEnd: number, 96 | fixValue: number = 0 97 | ) { 98 | if (xStart > xEnd) { 99 | const tmp = xEnd; 100 | xEnd = xStart; 101 | xStart = tmp; 102 | } 103 | const width = toFixed(xEnd - xStart); 104 | 105 | // Only rulers have a div child, helpers don't 106 | // Make sure to update the value for rulers only 107 | if ($el.firstChild) { 108 | $el.firstChild.textContent = width > 0 ? width.toString() : ""; 109 | } 110 | 111 | setPosition($el, xStart, yStart - fixValue, width, 0); 112 | } 113 | 114 | function setVerticalRuler( 115 | $el: HTMLDivElement, 116 | xStart: number, 117 | yStart: number, 118 | yEnd: number, 119 | fixValue: number = 0 120 | ) { 121 | if (yStart > yEnd) { 122 | const tmp = yEnd; 123 | yEnd = yStart; 124 | yStart = tmp; 125 | } 126 | 127 | const height = toFixed(yEnd - yStart); 128 | 129 | // Only rulers have a div child, helpers don't 130 | // Make sure to update the value for rulers only 131 | if ($el.firstChild) { 132 | $el.firstChild.textContent = height > 0 ? height.toString() : ""; 133 | } 134 | 135 | setPosition($el, xStart - fixValue, yStart, 0, height); 136 | } 137 | 138 | class Tailor { 139 | toggleKey: ToggleKey; 140 | 141 | $elementsToReset: HTMLDivElement[]; 142 | $rulers: HTMLDivElement[]; 143 | $helpers: HTMLDivElement[]; 144 | 145 | $tailor: HTMLDivElement; 146 | $mask: HTMLDivElement; 147 | $highlight: HTMLDivElement; 148 | $padding: HTMLDivElement; 149 | $margin: HTMLDivElement; 150 | $toMask: HTMLDivElement; 151 | 152 | $xRuler: HTMLDivElement; 153 | $xRuler2: HTMLDivElement; 154 | 155 | $yRuler: HTMLDivElement; 156 | $yRuler2: HTMLDivElement; 157 | 158 | $xRulerHelper: HTMLDivElement; 159 | $xRulerHelper2: HTMLDivElement; 160 | 161 | $yRulerHelper: HTMLDivElement; 162 | $yRulerHelper2: HTMLDivElement; 163 | 164 | $panel: HTMLDivElement; 165 | 166 | $current: HTMLElement | null = null; 167 | $measureTo: HTMLElement | null = null; 168 | 169 | selected: boolean = false; 170 | 171 | constructor(toggleKey: ToggleKey = "Alt") { 172 | this.toggleKey = toggleKey; 173 | 174 | // Create elements 175 | this.$mask = div({ class: "__tailor-mask" }); 176 | this.$toMask = div({ class: "__tailor-to-mask" }); 177 | 178 | this.$margin = div({ class: "__tailor-margin" }); 179 | this.$padding = div({ class: "__tailor-padding" }); 180 | this.$highlight = div({ class: "__tailor-highlight" }, [this.$margin, this.$padding]); 181 | 182 | this.$xRuler = div({ class: "__tailor-ruler __tailor-ruler--x" }, div()); 183 | this.$xRuler2 = div({ class: "__tailor-ruler __tailor-ruler--x" }, div()); 184 | 185 | this.$yRuler = div({ class: "__tailor-ruler __tailor-ruler--y" }, div()); 186 | this.$yRuler2 = div({ class: "__tailor-ruler __tailor-ruler--y" }, div()); 187 | 188 | this.$xRulerHelper = div({ class: "__tailor-ruler-helper __tailor-ruler-helper--x" }); 189 | this.$xRulerHelper2 = div({ class: "__tailor-ruler-helper __tailor-ruler-helper--x" }); 190 | 191 | this.$yRulerHelper = div({ class: "__tailor-ruler-helper __tailor-ruler-helper--y" }); 192 | this.$yRulerHelper2 = div({ class: "__tailor-ruler-helper __tailor-ruler-helper--y" }); 193 | 194 | this.$panel = div({ class: "__tailor-panel" }); 195 | 196 | // Save all rulers so we can disable them all together 197 | this.$rulers = [this.$xRuler, this.$xRuler2, this.$yRuler, this.$yRuler2]; 198 | this.$helpers = [ 199 | this.$xRulerHelper, 200 | this.$xRulerHelper2, 201 | this.$yRulerHelper, 202 | this.$yRulerHelper2, 203 | ]; 204 | 205 | this.$tailor = div({ class: "__tailor" }, [ 206 | this.$mask, 207 | this.$highlight, 208 | this.$toMask, 209 | ...this.$rulers, 210 | ...this.$helpers, 211 | this.$panel, 212 | ]); 213 | 214 | // Save other elements that need position reset 215 | this.$elementsToReset = [this.$mask, this.$highlight, this.$margin, this.$toMask]; 216 | 217 | // Singleton 218 | if ((window as any).__tailor_instance) { 219 | return (window as any).__tailor_instance as Tailor; 220 | } 221 | 222 | document.body.append(this.$tailor); 223 | 224 | window.addEventListener("keydown", this.handleKeyDown); 225 | window.addEventListener("keyup", this.handleKeyUp); 226 | window.addEventListener("blur", this.handleWindowBlur); 227 | 228 | (window as any).__tailor_instance = this; 229 | console.log( 230 | "%c T ", 231 | "background-color: #0b99ff; color: white; line-height: 17px; display: inline-block;", 232 | "Tailor initialized" 233 | ); 234 | } 235 | 236 | // ----- External API ----- // 237 | 238 | updateToggleKey(toggleKey: ToggleKey) { 239 | this.toggleKey = toggleKey; 240 | } 241 | 242 | // ----- CONTROLS ----- // 243 | 244 | enable() { 245 | this.$panel.replaceChildren(span({}, ["Tailor"]), " ready"); 246 | this.$tailor.style.display = "block"; 247 | 248 | // To prevent guidelines overflowing and creating scrollbars 249 | this.$tailor.style.width = document.body.scrollWidth + "px"; 250 | this.$tailor.style.height = document.body.scrollHeight + "px"; 251 | 252 | // Click and mousedown are using "capture = true" to prevent clicks on interactive elements 253 | // That way we can still measure without clicking and navigating from the page 254 | window.addEventListener("mousedown", this.handleMouseDown, true); 255 | window.addEventListener("click", this.handleClick, true); 256 | window.addEventListener("mousemove", this.handleMouseMove); 257 | window.addEventListener("contextmenu", this.handleContextMenu); 258 | } 259 | 260 | disable() { 261 | window.removeEventListener("mousedown", this.handleMouseDown, true); 262 | window.removeEventListener("click", this.handleClick, true); 263 | window.removeEventListener("mousemove", this.handleMouseMove); 264 | window.removeEventListener("contextmenu", this.handleContextMenu); 265 | 266 | this.$elementsToReset.forEach(($element) => { 267 | $element.setAttribute("style", ""); 268 | }); 269 | this.$margin.textContent = ""; 270 | this.$padding.textContent = ""; 271 | this.resetRulers(); 272 | 273 | this.$tailor.style.display = "none"; 274 | this.$current = null; 275 | this.selected = false; 276 | this.$tailor.classList.remove("__tailor--measuring"); 277 | this.$measureTo = null; 278 | } 279 | 280 | resetRulers() { 281 | this.$rulers.forEach(($ruler) => { 282 | $ruler.setAttribute("style", ""); 283 | // All rulers have a div child 284 | ($ruler.firstChild as HTMLDivElement).textContent = ""; 285 | }); 286 | this.$helpers.forEach(($helper) => { 287 | $helper.setAttribute("style", ""); 288 | }); 289 | } 290 | 291 | destroy() { 292 | this.disable(); 293 | this.$tailor.remove(); 294 | delete (window as any).__tailor_instance; 295 | window.removeEventListener("keyup", this.handleKeyUp); 296 | window.removeEventListener("keydown", this.handleKeyDown); 297 | window.removeEventListener("blur", this.handleWindowBlur); 298 | } 299 | 300 | // ----- EVENT HANDLERS ----- // 301 | 302 | handleContextMenu = (e: Event) => { 303 | e.preventDefault(); 304 | }; 305 | 306 | handleKeyDown = (e: KeyboardEvent) => { 307 | if (e.repeat) { 308 | return; 309 | } 310 | if (e.key === this.toggleKey) { 311 | this.enable(); 312 | } 313 | }; 314 | 315 | handleKeyUp = (e: KeyboardEvent) => { 316 | if (e.key === this.toggleKey) { 317 | this.disable(); 318 | } 319 | }; 320 | 321 | handleWindowBlur = () => { 322 | this.disable(); 323 | }; 324 | 325 | handleMouseMove = (e: MouseEvent) => { 326 | // TODO verify using EventTarget instead of casting to HTMLElement 327 | const $target = e.target as HTMLElement; 328 | if (this.selected) { 329 | if (this.$measureTo !== $target && this.$current && this.$current !== $target) { 330 | this.measureDistance(this.$current, $target); 331 | this.updatePanel($target); 332 | } 333 | } else if (this.$current !== e.target) { 334 | this.$current = $target; 335 | 336 | this.highlightElement(this.$current); 337 | this.updatePanel(this.$current); 338 | } 339 | }; 340 | 341 | handleClick = (e: MouseEvent) => { 342 | e.preventDefault(); 343 | e.stopPropagation(); 344 | }; 345 | 346 | handleMouseDown = (e: MouseEvent) => { 347 | e.preventDefault(); 348 | e.stopPropagation(); 349 | 350 | this.selected = true; 351 | this.$tailor.classList.add("__tailor--measuring"); 352 | this.$current = e.target as HTMLElement; 353 | this.highlightElement(this.$current); 354 | // reset to-mask when selecting a new element 355 | this.$toMask.setAttribute("style", ""); 356 | }; 357 | 358 | // ----- MAIN ----- // 359 | 360 | highlightElement($el: HTMLElement) { 361 | this.resetRulers(); 362 | 363 | const style = getComputedStyle($el); 364 | 365 | const outerBox = getRect($el); 366 | const box = this.selected ? outerBox : getBox($el); 367 | 368 | const { $mask, $highlight, $margin, $padding } = this; 369 | 370 | const margin = { 371 | top: parseFloat(style.marginTop), 372 | bottom: parseFloat(style.marginBottom), 373 | left: parseFloat(style.marginLeft), 374 | right: parseFloat(style.marginRight), 375 | inline: parseFloat(style.marginLeft) + parseFloat(style.marginRight), 376 | block: parseFloat(style.marginTop) + parseFloat(style.marginBottom), 377 | }; 378 | const padding = { 379 | top: parseFloat(style.paddingTop), 380 | bottom: parseFloat(style.paddingBottom), 381 | left: parseFloat(style.paddingLeft), 382 | right: parseFloat(style.paddingRight), 383 | inline: parseFloat(style.paddingLeft) + parseFloat(style.paddingRight), 384 | block: parseFloat(style.paddingTop) + parseFloat(style.paddingBottom), 385 | }; 386 | 387 | setPosition($highlight, box.x, box.y, box.width, box.height); 388 | 389 | setPosition($mask, outerBox.x, outerBox.y, outerBox.width, outerBox.height); 390 | 391 | $highlight.style.paddingLeft = style.paddingLeft; 392 | $highlight.style.paddingRight = style.paddingRight; 393 | $highlight.style.paddingTop = style.paddingTop; 394 | $highlight.style.paddingBottom = style.paddingBottom; 395 | 396 | $highlight.style.transform = this.selected ? "" : style.transform; 397 | 398 | setPosition( 399 | $margin, 400 | -margin.left, 401 | -margin.top, 402 | box.width + margin.inline, 403 | box.height + margin.block 404 | ); 405 | 406 | const marginElements: HTMLDivElement[] = []; 407 | const paddingElements: HTMLDivElement[] = []; 408 | const sides = ["top", "right", "bottom", "left"] as const; 409 | 410 | sides.forEach((side) => { 411 | if (margin[side]) { 412 | marginElements.push( 413 | div({ class: `__tailor-margin-label __tailor-margin-label--${side}` }, [ 414 | toFixed(margin[side]).toString(), 415 | ]) 416 | ); 417 | } 418 | if (padding[side]) { 419 | paddingElements.push( 420 | div({ class: `__tailor-padding-label __tailor-padding-label--${side}` }, [ 421 | toFixed(padding[side]).toString(), 422 | ]) 423 | ); 424 | } 425 | }); 426 | 427 | $margin.replaceChildren(...marginElements); 428 | $padding.replaceChildren(...paddingElements); 429 | } 430 | 431 | measureDistance($current: HTMLElement, $measureTo: HTMLElement) { 432 | const from = getRect($current); 433 | const to = getRect($measureTo); 434 | 435 | const { 436 | $xRuler, 437 | $xRuler2, 438 | $yRuler, 439 | $yRuler2, 440 | $xRulerHelper, 441 | $xRulerHelper2, 442 | $yRulerHelper, 443 | $yRulerHelper2, 444 | } = this; 445 | 446 | if ($measureTo) { 447 | setPosition(this.$toMask, to.left, to.top, to.width, to.height); 448 | } 449 | 450 | const positions: Record< 451 | | "horizontal1" 452 | | "horizontal2" 453 | | "vertical1" 454 | | "vertical2" 455 | | "hHelper1" 456 | | "hHelper2" 457 | | "vHelper1" 458 | | "vHelper2", 459 | [number, number, number, number] 460 | > = { 461 | horizontal1: [0, 0, 0, 0], 462 | horizontal2: [0, 0, 0, 0], 463 | vertical1: [0, 0, 0, 0], 464 | vertical2: [0, 0, 0, 0], 465 | hHelper1: [0, 0, 0, 0], 466 | hHelper2: [0, 0, 0, 0], 467 | vHelper1: [0, 0, 0, 0], 468 | vHelper2: [0, 0, 0, 0], 469 | }; 470 | 471 | this.$measureTo = $measureTo; 472 | 473 | const isAbove = from.bottom <= to.top; 474 | const isBelow = from.top >= to.bottom; 475 | 476 | const intersectsVertically = !isAbove && !isBelow; 477 | 478 | const isLeft = from.right <= to.left; 479 | const isRight = from.left >= to.right; 480 | 481 | const intersectsHorizontally = !isLeft && !isRight; 482 | 483 | const midFrom: Vector = { 484 | x: from.left + from.width * 0.5, 485 | y: from.top + from.height * 0.5, 486 | }; 487 | 488 | if (intersectsHorizontally) { 489 | const x1 = Math.max(from.left, to.left); 490 | const x2 = Math.min(from.right, to.right); 491 | 492 | midFrom.x = x1 + (x2 - x1) / 2; 493 | } 494 | 495 | if (intersectsVertically) { 496 | const y1 = Math.max(from.top, to.top); 497 | const y2 = Math.min(from.bottom, to.bottom); 498 | 499 | midFrom.y = y1 + (y2 - y1) / 2; 500 | } 501 | 502 | if (isAbove) { 503 | // fb - tt 504 | // isAbove 505 | positions.vertical1 = [midFrom.x, from.bottom, to.top, 0]; 506 | } else if (isBelow) { 507 | // ft - tb 508 | // isBelow 509 | positions.vertical1 = [midFrom.x, from.top, to.bottom, 1]; 510 | } else { 511 | // fb - tb 512 | // ft - tt 513 | // intersectsVertically 514 | positions.vertical1 = [midFrom.x, from.bottom, to.bottom, 1]; 515 | positions.vertical2 = [midFrom.x, from.top, to.top, 0]; 516 | } 517 | 518 | if (isLeft) { 519 | // fr - tl 520 | // isLeft 521 | positions.horizontal1 = [from.right, midFrom.y, to.left, 0]; 522 | } else if (isRight) { 523 | // fl - tr 524 | // isRight 525 | positions.horizontal1 = [from.left, midFrom.y, to.right, 1]; 526 | } else { 527 | // fr - tr 528 | // fl - tl 529 | // intersectsHorizontally 530 | positions.horizontal1 = [from.right, midFrom.y, to.right, 1]; 531 | positions.horizontal2 = [from.left, midFrom.y, to.left, 0]; 532 | } 533 | 534 | if (isLeft || isRight) { 535 | positions.hHelper1 = [midFrom.x, positions.vertical1[2], to.left, positions.vertical1[3]]; // yuradi isto 536 | if (intersectsVertically) { 537 | positions.hHelper2 = [midFrom.x, positions.vertical2[2], to.left, 0]; 538 | } 539 | } 540 | 541 | if (isAbove || isBelow) { 542 | positions.vHelper1 = [positions.horizontal1[2], midFrom.y, to.top, positions.horizontal1[3]]; 543 | if (intersectsHorizontally) { 544 | positions.vHelper2 = [positions.horizontal2[2], midFrom.y, to.top, 0]; 545 | } 546 | } 547 | 548 | // Rulers 549 | setVerticalRuler($yRuler, ...positions.vertical1); 550 | setVerticalRuler($yRuler2, ...positions.vertical2); 551 | setHorizontalRuler($xRuler, ...positions.horizontal1); 552 | setHorizontalRuler($xRuler2, ...positions.horizontal2); 553 | 554 | // Helpers 555 | setHorizontalRuler($xRulerHelper, ...positions.hHelper1); 556 | setHorizontalRuler($xRulerHelper2, ...positions.hHelper2); 557 | setVerticalRuler($yRulerHelper, ...positions.vHelper1); 558 | setVerticalRuler($yRulerHelper2, ...positions.vHelper2); 559 | } 560 | 561 | updatePanel($el: HTMLElement | SVGElement) { 562 | const style = getComputedStyle($el); 563 | const id = $el.id ? `#${$el.id}` : ""; 564 | 565 | let className = $el.getAttribute("class"); 566 | 567 | if (className) { 568 | className = `.${$el.getAttribute("class")?.split(" ").join(".")}`; 569 | } else { 570 | className = ""; 571 | } 572 | 573 | let font = style.fontFamily; 574 | const fontStack = style.fontFamily.split(", "); 575 | 576 | for (const i in fontStack) { 577 | if (document.fonts.check(`16px ${fontStack[i]}`)) { 578 | font = fontStack[i].replace(/"/g, ""); 579 | break; 580 | } 581 | } 582 | 583 | let height = style.height.replace("px", ""); 584 | let width = style.width.replace("px", ""); 585 | 586 | if (height === "auto" || width === "auto") { 587 | // For SVG elements use getBoundingClientRect 588 | if ($el instanceof SVGElement) { 589 | const rect = getRect($el); 590 | height = rect.height.toString(); 591 | width = rect.width.toString(); 592 | } else { 593 | height = $el.offsetHeight.toString(); 594 | width = $el.offsetWidth.toString(); 595 | } 596 | } 597 | 598 | this.$panel.replaceChildren( 599 | // Element tag and id/class 600 | span({}, [$el.tagName.toLowerCase()]), 601 | `${id}${className}`, 602 | // Dimensions 603 | div({}, [`${width}x${height}px`]), 604 | // Font 605 | div({}, [font]), 606 | div({}, [`${style.fontSize} ${style.lineHeight}`]), 607 | div({}, [`${style.fontWeight} ${style.fontStyle}`]) 608 | ); 609 | } 610 | } 611 | 612 | export default Tailor; 613 | -------------------------------------------------------------------------------- /docs/build/docs.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../src/index.ts", "../src/docs.ts"], 4 | "sourcesContent": ["type Vector = { x: number; y: number };\n\ntype Child = string | HTMLElement | SVGElement;\n\nfunction el(\n tagName: string,\n attributes: Record = {},\n children: Child | Child[] = []\n): HTMLElement {\n // Create element\n const $div = document.createElement(tagName);\n\n // If children is a single element, wrap it into array\n if (!Array.isArray(children)) {\n children = [children];\n }\n\n // Loop through and append children\n children.forEach((child) => {\n if (typeof child === \"string\") {\n $div.append(child);\n } else {\n $div.appendChild(child);\n }\n });\n\n // Sett HTML attributes\n for (const name in attributes) {\n $div.setAttribute(name, attributes[name]);\n }\n\n return $div;\n}\n\nconst div = (attributes: Record = {}, children: Child | Child[] = []) => {\n return el(\"div\", attributes, children) as HTMLDivElement;\n};\n\nconst span = (attributes: Record = {}, children: Child | Child[] = []) => {\n return el(\"span\", attributes, children) as HTMLSpanElement;\n};\n\ntype ToggleKey = \"Control\" | \"Meta\" | \"Alt\";\n\nfunction getRect($el: HTMLElement | SVGElement) {\n const rect = $el.getBoundingClientRect();\n rect.x += window.scrollX;\n rect.y += window.scrollY;\n return rect;\n}\n\nfunction getBox($el: HTMLElement | SVGElement) {\n if ($el instanceof SVGElement) {\n return getRect($el);\n }\n\n let y = 0;\n let x = 0;\n let $element: HTMLElement | null = $el;\n const width = $el.offsetWidth;\n const height = $el.offsetHeight;\n\n do {\n y += $element.offsetTop || 0;\n x += $element.offsetLeft || 0;\n y -= $element.scrollTop || 0;\n x -= $element.scrollLeft || 0;\n // TODO check if this is safe to cast\n $element = $element.offsetParent as HTMLElement;\n } while ($element);\n\n return {\n x,\n y,\n width,\n height,\n };\n}\n\nfunction toFixed(n: number, decimals: number = 1) {\n return +n.toFixed(decimals);\n}\n\nfunction setPosition($el: HTMLElement, left: number, top: number, width: number, height: number) {\n $el.style.left = `${left}px`;\n $el.style.top = `${top}px`;\n $el.style.width = `${width}px`;\n $el.style.height = `${height}px`;\n}\n\nfunction setHorizontalRuler(\n $el: HTMLDivElement,\n xStart: number,\n yStart: number,\n xEnd: number,\n fixValue: number = 0\n) {\n if (xStart > xEnd) {\n const tmp = xEnd;\n xEnd = xStart;\n xStart = tmp;\n }\n const width = toFixed(xEnd - xStart);\n\n // Only rulers have a div child, helpers don't\n // Make sure to update the value for rulers only\n if ($el.firstChild) {\n $el.firstChild.textContent = width > 0 ? width.toString() : \"\";\n }\n\n setPosition($el, xStart, yStart - fixValue, width, 0);\n}\n\nfunction setVerticalRuler(\n $el: HTMLDivElement,\n xStart: number,\n yStart: number,\n yEnd: number,\n fixValue: number = 0\n) {\n if (yStart > yEnd) {\n const tmp = yEnd;\n yEnd = yStart;\n yStart = tmp;\n }\n\n const height = toFixed(yEnd - yStart);\n\n // Only rulers have a div child, helpers don't\n // Make sure to update the value for rulers only\n if ($el.firstChild) {\n $el.firstChild.textContent = height > 0 ? height.toString() : \"\";\n }\n\n setPosition($el, xStart - fixValue, yStart, 0, height);\n}\n\nclass Tailor {\n toggleKey: ToggleKey;\n\n $elementsToReset: HTMLDivElement[];\n $rulers: HTMLDivElement[];\n $helpers: HTMLDivElement[];\n\n $tailor: HTMLDivElement;\n $mask: HTMLDivElement;\n $highlight: HTMLDivElement;\n $padding: HTMLDivElement;\n $margin: HTMLDivElement;\n $toMask: HTMLDivElement;\n\n $xRuler: HTMLDivElement;\n $xRuler2: HTMLDivElement;\n\n $yRuler: HTMLDivElement;\n $yRuler2: HTMLDivElement;\n\n $xRulerHelper: HTMLDivElement;\n $xRulerHelper2: HTMLDivElement;\n\n $yRulerHelper: HTMLDivElement;\n $yRulerHelper2: HTMLDivElement;\n\n $panel: HTMLDivElement;\n\n $current: HTMLElement | null = null;\n $measureTo: HTMLElement | null = null;\n\n selected: boolean = false;\n\n constructor(toggleKey: ToggleKey = \"Alt\") {\n this.toggleKey = toggleKey;\n\n // Create elements\n this.$mask = div({ class: \"__tailor-mask\" });\n this.$toMask = div({ class: \"__tailor-to-mask\" });\n\n this.$margin = div({ class: \"__tailor-margin\" });\n this.$padding = div({ class: \"__tailor-padding\" });\n this.$highlight = div({ class: \"__tailor-highlight\" }, [this.$margin, this.$padding]);\n\n this.$xRuler = div({ class: \"__tailor-ruler __tailor-ruler--x\" }, div());\n this.$xRuler2 = div({ class: \"__tailor-ruler __tailor-ruler--x\" }, div());\n\n this.$yRuler = div({ class: \"__tailor-ruler __tailor-ruler--y\" }, div());\n this.$yRuler2 = div({ class: \"__tailor-ruler __tailor-ruler--y\" }, div());\n\n this.$xRulerHelper = div({ class: \"__tailor-ruler-helper __tailor-ruler-helper--x\" });\n this.$xRulerHelper2 = div({ class: \"__tailor-ruler-helper __tailor-ruler-helper--x\" });\n\n this.$yRulerHelper = div({ class: \"__tailor-ruler-helper __tailor-ruler-helper--y\" });\n this.$yRulerHelper2 = div({ class: \"__tailor-ruler-helper __tailor-ruler-helper--y\" });\n\n this.$panel = div({ class: \"__tailor-panel\" });\n\n // Save all rulers so we can disable them all together\n this.$rulers = [this.$xRuler, this.$xRuler2, this.$yRuler, this.$yRuler2];\n this.$helpers = [\n this.$xRulerHelper,\n this.$xRulerHelper2,\n this.$yRulerHelper,\n this.$yRulerHelper2,\n ];\n\n this.$tailor = div({ class: \"__tailor\" }, [\n this.$mask,\n this.$highlight,\n this.$toMask,\n ...this.$rulers,\n ...this.$helpers,\n this.$panel,\n ]);\n\n // Save other elements that need position reset\n this.$elementsToReset = [this.$mask, this.$highlight, this.$margin, this.$toMask];\n\n // Singleton\n if ((window as any).__tailor_instance) {\n return (window as any).__tailor_instance as Tailor;\n }\n\n document.body.append(this.$tailor);\n\n window.addEventListener(\"keydown\", this.handleKeyDown);\n window.addEventListener(\"keyup\", this.handleKeyUp);\n window.addEventListener(\"blur\", this.handleWindowBlur);\n\n (window as any).__tailor_instance = this;\n console.log(\n \"%c T \",\n \"background-color: #0b99ff; color: white; line-height: 17px; display: inline-block;\",\n \"Tailor initialized\"\n );\n }\n\n // ----- External API ----- //\n\n updateToggleKey(toggleKey: ToggleKey) {\n this.toggleKey = toggleKey;\n }\n\n // ----- CONTROLS ----- //\n\n enable() {\n this.$panel.replaceChildren(span({}, [\"Tailor\"]), \" ready\");\n this.$tailor.style.display = \"block\";\n\n // To prevent guidelines overflowing and creating scrollbars\n this.$tailor.style.width = document.body.scrollWidth + \"px\";\n this.$tailor.style.height = document.body.scrollHeight + \"px\";\n\n // Click and mousedown are using \"capture = true\" to prevent clicks on interactive elements\n // That way we can still measure without clicking and navigating from the page\n window.addEventListener(\"mousedown\", this.handleMouseDown, true);\n window.addEventListener(\"click\", this.handleClick, true);\n window.addEventListener(\"mousemove\", this.handleMouseMove);\n window.addEventListener(\"contextmenu\", this.handleContextMenu);\n }\n\n disable() {\n window.removeEventListener(\"mousedown\", this.handleMouseDown, true);\n window.removeEventListener(\"click\", this.handleClick, true);\n window.removeEventListener(\"mousemove\", this.handleMouseMove);\n window.removeEventListener(\"contextmenu\", this.handleContextMenu);\n\n this.$elementsToReset.forEach(($element) => {\n $element.setAttribute(\"style\", \"\");\n });\n this.$margin.textContent = \"\";\n this.$padding.textContent = \"\";\n this.resetRulers();\n\n this.$tailor.style.display = \"none\";\n this.$current = null;\n this.selected = false;\n this.$tailor.classList.remove(\"__tailor--measuring\");\n this.$measureTo = null;\n }\n\n resetRulers() {\n this.$rulers.forEach(($ruler) => {\n $ruler.setAttribute(\"style\", \"\");\n // All rulers have a div child\n ($ruler.firstChild as HTMLDivElement).textContent = \"\";\n });\n this.$helpers.forEach(($helper) => {\n $helper.setAttribute(\"style\", \"\");\n });\n }\n\n destroy() {\n this.disable();\n this.$tailor.remove();\n delete (window as any).__tailor_instance;\n window.removeEventListener(\"keyup\", this.handleKeyUp);\n window.removeEventListener(\"keydown\", this.handleKeyDown);\n window.removeEventListener(\"blur\", this.handleWindowBlur);\n }\n\n // ----- EVENT HANDLERS ----- //\n\n handleContextMenu = (e: Event) => {\n e.preventDefault();\n };\n\n handleKeyDown = (e: KeyboardEvent) => {\n if (e.repeat) {\n return;\n }\n if (e.key === this.toggleKey) {\n this.enable();\n }\n };\n\n handleKeyUp = (e: KeyboardEvent) => {\n if (e.key === this.toggleKey) {\n this.disable();\n }\n };\n\n handleWindowBlur = () => {\n this.disable();\n };\n\n handleMouseMove = (e: MouseEvent) => {\n // TODO verify using EventTarget instead of casting to HTMLElement\n const $target = e.target as HTMLElement;\n if (this.selected) {\n if (this.$measureTo !== $target && this.$current && this.$current !== $target) {\n this.measureDistance(this.$current, $target);\n this.updatePanel($target);\n }\n } else if (this.$current !== e.target) {\n this.$current = $target;\n\n this.highlightElement(this.$current);\n this.updatePanel(this.$current);\n }\n };\n\n handleClick = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n };\n\n handleMouseDown = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n this.selected = true;\n this.$tailor.classList.add(\"__tailor--measuring\");\n this.$current = e.target as HTMLElement;\n this.highlightElement(this.$current);\n // reset to-mask when selecting a new element\n this.$toMask.setAttribute(\"style\", \"\");\n };\n\n // ----- MAIN ----- //\n\n highlightElement($el: HTMLElement) {\n this.resetRulers();\n\n const style = getComputedStyle($el);\n\n const outerBox = getRect($el);\n const box = this.selected ? outerBox : getBox($el);\n\n const { $mask, $highlight, $margin, $padding } = this;\n\n const margin = {\n top: parseFloat(style.marginTop),\n bottom: parseFloat(style.marginBottom),\n left: parseFloat(style.marginLeft),\n right: parseFloat(style.marginRight),\n inline: parseFloat(style.marginLeft) + parseFloat(style.marginRight),\n block: parseFloat(style.marginTop) + parseFloat(style.marginBottom),\n };\n const padding = {\n top: parseFloat(style.paddingTop),\n bottom: parseFloat(style.paddingBottom),\n left: parseFloat(style.paddingLeft),\n right: parseFloat(style.paddingRight),\n inline: parseFloat(style.paddingLeft) + parseFloat(style.paddingRight),\n block: parseFloat(style.paddingTop) + parseFloat(style.paddingBottom),\n };\n\n setPosition($highlight, box.x, box.y, box.width, box.height);\n\n setPosition($mask, outerBox.x, outerBox.y, outerBox.width, outerBox.height);\n\n $highlight.style.paddingLeft = style.paddingLeft;\n $highlight.style.paddingRight = style.paddingRight;\n $highlight.style.paddingTop = style.paddingTop;\n $highlight.style.paddingBottom = style.paddingBottom;\n\n $highlight.style.transform = this.selected ? \"\" : style.transform;\n\n setPosition(\n $margin,\n -margin.left,\n -margin.top,\n box.width + margin.inline,\n box.height + margin.block\n );\n\n const marginElements: HTMLDivElement[] = [];\n const paddingElements: HTMLDivElement[] = [];\n const sides = [\"top\", \"right\", \"bottom\", \"left\"] as const;\n\n sides.forEach((side) => {\n if (margin[side]) {\n marginElements.push(\n div({ class: `__tailor-margin-label __tailor-margin-label--${side}` }, [\n toFixed(margin[side]).toString(),\n ])\n );\n }\n if (padding[side]) {\n paddingElements.push(\n div({ class: `__tailor-padding-label __tailor-padding-label--${side}` }, [\n toFixed(padding[side]).toString(),\n ])\n );\n }\n });\n\n $margin.replaceChildren(...marginElements);\n $padding.replaceChildren(...paddingElements);\n }\n\n measureDistance($current: HTMLElement, $measureTo: HTMLElement) {\n const from = getRect($current);\n const to = getRect($measureTo);\n\n const {\n $xRuler,\n $xRuler2,\n $yRuler,\n $yRuler2,\n $xRulerHelper,\n $xRulerHelper2,\n $yRulerHelper,\n $yRulerHelper2,\n } = this;\n\n if ($measureTo) {\n setPosition(this.$toMask, to.left, to.top, to.width, to.height);\n }\n\n const positions: Record<\n | \"horizontal1\"\n | \"horizontal2\"\n | \"vertical1\"\n | \"vertical2\"\n | \"hHelper1\"\n | \"hHelper2\"\n | \"vHelper1\"\n | \"vHelper2\",\n [number, number, number, number]\n > = {\n horizontal1: [0, 0, 0, 0],\n horizontal2: [0, 0, 0, 0],\n vertical1: [0, 0, 0, 0],\n vertical2: [0, 0, 0, 0],\n hHelper1: [0, 0, 0, 0],\n hHelper2: [0, 0, 0, 0],\n vHelper1: [0, 0, 0, 0],\n vHelper2: [0, 0, 0, 0],\n };\n\n this.$measureTo = $measureTo;\n\n const isAbove = from.bottom <= to.top;\n const isBelow = from.top >= to.bottom;\n\n const intersectsVertically = !isAbove && !isBelow;\n\n const isLeft = from.right <= to.left;\n const isRight = from.left >= to.right;\n\n const intersectsHorizontally = !isLeft && !isRight;\n\n const midFrom: Vector = {\n x: from.left + from.width * 0.5,\n y: from.top + from.height * 0.5,\n };\n\n if (intersectsHorizontally) {\n const x1 = Math.max(from.left, to.left);\n const x2 = Math.min(from.right, to.right);\n\n midFrom.x = x1 + (x2 - x1) / 2;\n }\n\n if (intersectsVertically) {\n const y1 = Math.max(from.top, to.top);\n const y2 = Math.min(from.bottom, to.bottom);\n\n midFrom.y = y1 + (y2 - y1) / 2;\n }\n\n if (isAbove) {\n // fb - tt\n // isAbove\n positions.vertical1 = [midFrom.x, from.bottom, to.top, 0];\n } else if (isBelow) {\n // ft - tb\n // isBelow\n positions.vertical1 = [midFrom.x, from.top, to.bottom, 1];\n } else {\n // fb - tb\n // ft - tt\n // intersectsVertically\n positions.vertical1 = [midFrom.x, from.bottom, to.bottom, 1];\n positions.vertical2 = [midFrom.x, from.top, to.top, 0];\n }\n\n if (isLeft) {\n // fr - tl\n // isLeft\n positions.horizontal1 = [from.right, midFrom.y, to.left, 0];\n } else if (isRight) {\n // fl - tr\n // isRight\n positions.horizontal1 = [from.left, midFrom.y, to.right, 1];\n } else {\n // fr - tr\n // fl - tl\n // intersectsHorizontally\n positions.horizontal1 = [from.right, midFrom.y, to.right, 1];\n positions.horizontal2 = [from.left, midFrom.y, to.left, 0];\n }\n\n if (isLeft || isRight) {\n positions.hHelper1 = [midFrom.x, positions.vertical1[2], to.left, positions.vertical1[3]]; // yuradi isto\n if (intersectsVertically) {\n positions.hHelper2 = [midFrom.x, positions.vertical2[2], to.left, 0];\n }\n }\n\n if (isAbove || isBelow) {\n positions.vHelper1 = [positions.horizontal1[2], midFrom.y, to.top, positions.horizontal1[3]];\n if (intersectsHorizontally) {\n positions.vHelper2 = [positions.horizontal2[2], midFrom.y, to.top, 0];\n }\n }\n\n // Rulers\n setVerticalRuler($yRuler, ...positions.vertical1);\n setVerticalRuler($yRuler2, ...positions.vertical2);\n setHorizontalRuler($xRuler, ...positions.horizontal1);\n setHorizontalRuler($xRuler2, ...positions.horizontal2);\n\n // Helpers\n setHorizontalRuler($xRulerHelper, ...positions.hHelper1);\n setHorizontalRuler($xRulerHelper2, ...positions.hHelper2);\n setVerticalRuler($yRulerHelper, ...positions.vHelper1);\n setVerticalRuler($yRulerHelper2, ...positions.vHelper2);\n }\n\n updatePanel($el: HTMLElement | SVGElement) {\n const style = getComputedStyle($el);\n const id = $el.id ? `#${$el.id}` : \"\";\n\n let className = $el.getAttribute(\"class\");\n\n if (className) {\n className = `.${$el.getAttribute(\"class\")?.split(\" \").join(\".\")}`;\n } else {\n className = \"\";\n }\n\n let font = style.fontFamily;\n const fontStack = style.fontFamily.split(\", \");\n\n for (const i in fontStack) {\n if (document.fonts.check(`16px ${fontStack[i]}`)) {\n font = fontStack[i].replace(/\"/g, \"\");\n break;\n }\n }\n\n let height = style.height.replace(\"px\", \"\");\n let width = style.width.replace(\"px\", \"\");\n\n if (height === \"auto\" || width === \"auto\") {\n // For SVG elements use getBoundingClientRect\n if ($el instanceof SVGElement) {\n const rect = getRect($el);\n height = rect.height.toString();\n width = rect.width.toString();\n } else {\n height = $el.offsetHeight.toString();\n width = $el.offsetWidth.toString();\n }\n }\n\n this.$panel.replaceChildren(\n // Element tag and id/class\n span({}, [$el.tagName.toLowerCase()]),\n `${id}${className}`,\n // Dimensions\n div({}, [`${width}x${height}px`]),\n // Font\n div({}, [font]),\n div({}, [`${style.fontSize} ${style.lineHeight}`]),\n div({}, [`${style.fontWeight} ${style.fontStyle}`])\n );\n }\n}\n\nexport default Tailor;\n", "import Tailor from \"../../src/index\";\n\nnew Tailor();\n"], 5 | "mappings": "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", 6 | "names": ["el", "tagName", "attributes", "children", "$div", "child", "name", "div", "span", "getRect", "$el", "rect", "getBox", "y", "x", "$element", "width", "height", "toFixed", "n", "decimals", "setPosition", "left", "top", "setHorizontalRuler", "xStart", "yStart", "xEnd", "fixValue", "tmp", "setVerticalRuler", "yEnd", "Tailor", "toggleKey", "$target", "$ruler", "$helper", "style", "outerBox", "box", "$mask", "$highlight", "$margin", "$padding", "margin", "padding", "marginElements", "paddingElements", "side", "$current", "$measureTo", "from", "to", "$xRuler", "$xRuler2", "$yRuler", "$yRuler2", "$xRulerHelper", "$xRulerHelper2", "$yRulerHelper", "$yRulerHelper2", "positions", "isAbove", "isBelow", "intersectsVertically", "isLeft", "isRight", "intersectsHorizontally", "midFrom", "x1", "x2", "y1", "y2", "id", "className", "font", "fontStack", "i", "src_default", "src_default"] 7 | } 8 | --------------------------------------------------------------------------------