├── .editorconfig ├── .firebaserc ├── .gcloudignore ├── .github └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTE.md ├── LICENSE ├── VisBug ├── VisBug Extension │ ├── Info.plist │ ├── SafariWebExtensionHandler.swift │ └── VisBug_Extension.entitlements ├── VisBug.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── VisBug.xcscheme │ └── xcuserdata │ │ └── argyle.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── VisBug │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── VisBug1x-1.png │ │ ├── VisBug1x-2.png │ │ ├── VisBug1x.png │ │ ├── VisBug2x-1.png │ │ ├── VisBug2x-4.png │ │ └── VisBug2x.png │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── Info.plist │ ├── ViewController.swift │ └── VisBug.entitlements ├── app.yaml ├── app ├── 404.html ├── assets │ ├── arrow.png │ ├── hero-1.svg │ ├── hero-2.svg │ ├── hero-3.svg │ ├── hero-4.svg │ └── visbug.png ├── components │ ├── _variables.css │ ├── _variables_dark.css │ ├── _variables_light.css │ ├── hotkey-map │ │ ├── accessibility.element.js │ │ ├── align.element.js │ │ ├── base.element.css │ │ ├── base.element.js │ │ ├── base.element_dark.css │ │ ├── base.element_light.css │ │ ├── boxshadow.element.js │ │ ├── font.element.js │ │ ├── guides.element.js │ │ ├── hotkeys.element.js │ │ ├── hueshift.element.js │ │ ├── inspector.element.js │ │ ├── margin.element.js │ │ ├── move.element.js │ │ ├── padding.element.js │ │ ├── position.element.js │ │ ├── search.element.js │ │ └── text.element.js │ ├── index.js │ ├── metatip │ │ ├── ally.element.js │ │ ├── metatip.element.css │ │ ├── metatip.element.js │ │ ├── metatip.element_dark.css │ │ └── metatip.element_light.css │ ├── selection │ │ ├── box-model.element.css │ │ ├── box-model.element.js │ │ ├── corners.element.css │ │ ├── corners.element.js │ │ ├── distance.element.css │ │ ├── distance.element.js │ │ ├── gridlines.element.css │ │ ├── gridlines.element.js │ │ ├── grip.element.css │ │ ├── grip.element.js │ │ ├── handle.element.css │ │ ├── handle.element.js │ │ ├── handles.element.css │ │ ├── handles.element.js │ │ ├── hover.element.css │ │ ├── hover.element.js │ │ ├── label.element.css │ │ ├── label.element.js │ │ ├── offscreenLabel.element.css │ │ ├── offscreenLabel.element.js │ │ ├── overlay.element.css │ │ └── overlay.element.js │ ├── styles.store.js │ └── vis-bug │ │ ├── model.js │ │ ├── vis-bug.css │ │ ├── vis-bug.element.css │ │ ├── vis-bug.element.js │ │ ├── vis-bug.element_dark.css │ │ ├── vis-bug.element_light.css │ │ ├── vis-bug.icons.js │ │ └── vis-bug.test.js ├── demo │ ├── artboard.css │ ├── card.css │ ├── index.css │ ├── layout.css │ ├── mock-ad.css │ ├── multi-select.css │ ├── shapes.css │ └── typography.css ├── extension.css ├── favicon.ico ├── features │ ├── accessibility.js │ ├── accessibility.test.js │ ├── boxshadow.js │ ├── boxshadow.test.js │ ├── color.js │ ├── flex.js │ ├── flex.test.js │ ├── font.js │ ├── font.test.js │ ├── guides.js │ ├── guides.test.js │ ├── hueshift.js │ ├── imageswap.js │ ├── index.js │ ├── margin.js │ ├── margin.test.js │ ├── measurements.js │ ├── metatip.js │ ├── metatip.test.js │ ├── move.js │ ├── move.test.js │ ├── padding.js │ ├── padding.test.js │ ├── position.js │ ├── position.test.js │ ├── screenshot.js │ ├── search.js │ ├── selectable.js │ ├── selectable.test.js │ ├── text.js │ └── text.test.js ├── index.css ├── index.html ├── index.js ├── plugins │ ├── _dynamic-registery.js │ ├── _registry.js │ ├── barrel-roll.js │ ├── blank-page.js │ ├── colorblind.js │ ├── construct.debug.js │ ├── construct.js │ ├── ct-head-scan.js │ ├── detect-overflows.js │ ├── expand-text.js │ ├── loop-through-widths.js │ ├── no-mouse-days.js │ ├── pesticide.js │ ├── placeholdifier.js │ ├── remove-css.js │ ├── revenge.js │ ├── shuffle.js │ ├── skeleton.js │ ├── tag-debugger.js │ ├── tota11y.js │ ├── wireframe.js │ └── zindex.js ├── tuts │ ├── accessibility.gif │ ├── align.gif │ ├── boxshadow.gif │ ├── font.gif │ ├── guides.gif │ ├── hueshift.gif │ ├── inspector.gif │ ├── margin.gif │ ├── move.gif │ ├── padding.gif │ ├── position.gif │ ├── search.gif │ └── text.gif └── utilities │ ├── accessibility.js │ ├── colors.js │ ├── common.js │ ├── cross-browser.js │ ├── design-properties.js │ ├── index.js │ ├── isFixed.js │ ├── numbers.js │ ├── scheme.js │ ├── strings.js │ ├── styles.js │ └── window.js ├── assets ├── tuts_src │ ├── a11y.gif │ ├── edittext.gif │ ├── flexbox.gif │ ├── guides.gif │ ├── hueshift.gif │ ├── margin.gif │ ├── metatip.gif │ ├── move.gif │ ├── padding.gif │ ├── position.gif │ ├── search.gif │ ├── shadow.gif │ └── typography.gif ├── visbug.png ├── visbug.sketch └── visbug.svg ├── extension ├── build │ └── .keep ├── contextmenu │ ├── colormode.js │ ├── colorscheme.js │ └── launcher.js ├── icons │ ├── visbug-dev.png │ ├── visbug-original.png │ └── visbug.png ├── manifest.json ├── toolbar │ ├── eject.js │ ├── inject.js │ └── restore.js └── visbug.js ├── firebase.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── readme.md ├── rollup.config.mjs └── tests └── helpers.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [{*.md,*.json}] 18 | max_line_length = null -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "visbug" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ 18 | extension/ 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: Use Node.js 16.x 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 16.x 17 | 18 | - name: Git Init 19 | run: | 20 | git switch -c main 21 | git config --global user.email "argyle@google.com" 22 | git config --global user.name "Adam Argyle" 23 | 24 | - name: Test & Build 25 | run: | 26 | npm install 27 | npm run extension:release 28 | 29 | - name: Git Push 30 | uses: ad-m/github-push-action@master 31 | continue-on-error: true 32 | with: 33 | tags: true 34 | branch: 'main' 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Save Build As Artifact 38 | uses: actions/upload-artifact@v4 39 | continue-on-error: true 40 | with: 41 | name: VisBug 42 | path: extension/build/visbug.zip 43 | 44 | - name: Publish Chrome Extension 45 | uses: trmcnvn/chrome-addon@v2 46 | continue-on-error: true 47 | with: 48 | extension: cdockenadnadldjbbgcallicgledbeoc 49 | zip: extension/build/visbug.zip 50 | client-id: ${{ secrets.CHROME_CLIENT_ID }} 51 | client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} 52 | refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} 53 | 54 | - name: Firebase Deploy 55 | uses: pizzafox/firebase-action@1.0.7 56 | env: 57 | PROJECT_ID: "visbug" 58 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 59 | with: 60 | args: deploy 61 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - name: Use Node.js 16.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 16.x 18 | 19 | - name: Test & Build 20 | run: | 21 | npm ci 22 | npm run extension:build 23 | npm run test:ci 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules/ 4 | app/bundle.js 5 | app/bundle.min.js 6 | app/bundle.css 7 | extension/toolbar/bundle.min.js 8 | extension/toolbar/bundle.css 9 | extension/build 10 | extension/tuts 11 | VisBug/VisBug.xcodeproj/project.xcworkspace/xcuserdata/argyle.xcuserdatad/UserInterfaceState.xcuserstate 12 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /VisBug/VisBug Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | VisBug Extension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.Safari.web-extension 29 | NSExtensionPrincipalClass 30 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /VisBug/VisBug Extension/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // VisBug Extension 4 | // 5 | // Created by Adam Argyle on 11/2/20. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | let SFExtensionMessageKey = "message" 12 | 13 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 14 | 15 | func beginRequest(with context: NSExtensionContext) { 16 | let item = context.inputItems[0] as! NSExtensionItem 17 | let message = item.userInfo?[SFExtensionMessageKey] 18 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 19 | 20 | let response = NSExtensionItem() 21 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 22 | 23 | context.completeRequest(returningItems: [response], completionHandler: nil) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /VisBug/VisBug Extension/VisBug_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /VisBug/VisBug.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VisBug/VisBug.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VisBug/VisBug.xcodeproj/xcshareddata/xcschemes/VisBug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /VisBug/VisBug.xcodeproj/xcuserdata/argyle.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /VisBug/VisBug.xcodeproj/xcuserdata/argyle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | VisBug Extension.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | VisBug.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 8EBF957C2550B10A007B8136 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /VisBug/VisBug/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // VisBug 4 | // 5 | // Created by Adam Argyle on 11/3/20. 6 | // 7 | 8 | import Cocoa 9 | 10 | @NSApplicationMain 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | // Insert code here to initialize your application 15 | } 16 | 17 | func applicationWillTerminate(_ aNotification: Notification) { 18 | // Insert code here to tear down your application 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "filename" : "VisBug1x-2.png", 25 | "idiom" : "mac", 26 | "scale" : "1x", 27 | "size" : "128x128" 28 | }, 29 | { 30 | "filename" : "VisBug2x-4.png", 31 | "idiom" : "mac", 32 | "scale" : "2x", 33 | "size" : "128x128" 34 | }, 35 | { 36 | "filename" : "VisBug1x-1.png", 37 | "idiom" : "mac", 38 | "scale" : "1x", 39 | "size" : "256x256" 40 | }, 41 | { 42 | "filename" : "VisBug2x-1.png", 43 | "idiom" : "mac", 44 | "scale" : "2x", 45 | "size" : "256x256" 46 | }, 47 | { 48 | "filename" : "VisBug1x.png", 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "filename" : "VisBug2x.png", 55 | "idiom" : "mac", 56 | "scale" : "2x", 57 | "size" : "512x512" 58 | } 59 | ], 60 | "info" : { 61 | "author" : "xcode", 62 | "version" : 1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x-1.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x-2.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug1x.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x-1.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x-4.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/VisBug/VisBug/Assets.xcassets/AppIcon.appiconset/VisBug2x.png -------------------------------------------------------------------------------- /VisBug/VisBug/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VisBug/VisBug/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.developer-tools 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /VisBug/VisBug/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // VisBug 4 | // 5 | // Created by Adam Argyle on 11/2/20. 6 | // 7 | 8 | import Cocoa 9 | import SafariServices.SFSafariApplication 10 | import SafariServices.SFSafariExtensionManager 11 | 12 | let appName = "VisBug" 13 | let extensionBundleIdentifier = "argyleink.VisBug.Extension" 14 | 15 | class ViewController: NSViewController { 16 | 17 | @IBOutlet var appNameLabel: NSTextField! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | self.appNameLabel.stringValue = appName 22 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 23 | guard let state = state, error == nil else { 24 | // Insert code to inform the user that something went wrong. 25 | return 26 | } 27 | 28 | DispatchQueue.main.async { 29 | if (state.isEnabled) { 30 | self.appNameLabel.stringValue = "\(appName)'s extension is currently on." 31 | } else { 32 | self.appNameLabel.stringValue = "\(appName)'s extension is currently off. You can turn it on in Safari Extensions preferences." 33 | } 34 | } 35 | } 36 | } 37 | 38 | @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) { 39 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 40 | guard error == nil else { 41 | // Insert code to inform the user that something went wrong. 42 | return 43 | } 44 | 45 | DispatchQueue.main.async { 46 | NSApplication.shared.terminate(nil) 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /VisBug/VisBug/VisBug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | handlers: 6 | - url: /(.*\.css) 7 | mime_type: text/css 8 | static_files: app/\1 9 | upload: app/(.*\.css) 10 | 11 | - url: /(.*\.js) 12 | mime_type: text/javascript 13 | static_files: app/\1 14 | upload: app/(.*\.js) 15 | 16 | - url: /(.*\.(bmp|gif|ico|jpeg|jpg|png)) 17 | static_files: app/\1 18 | upload: app/(.*\.(bmp|gif|ico|jpeg|jpg|png)) 19 | 20 | - url: /(.*\.(svg|svgz)) 21 | mime_type: image/svg+xml 22 | static_files: app/\1 23 | upload: app/(.*\.(svg|svgz)) 24 | 25 | - url: /(.+)/ 26 | static_files: app/\1/index.html 27 | upload: app/(.*)/index.html 28 | 29 | - url: /(.+) 30 | static_files: app/\1/index.html 31 | upload: app/(.*)/index.html 32 | 33 | - url: / 34 | static_files: app/index.html 35 | upload: app/index.html 36 | -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /app/assets/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/assets/arrow.png -------------------------------------------------------------------------------- /app/assets/visbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/assets/visbug.png -------------------------------------------------------------------------------- /app/components/_variables.css: -------------------------------------------------------------------------------- 1 | @import "open-props/shadows.shadow.min.css"; 2 | @import "open-props/borders.shadow.min.css"; 3 | @import "open-props/gray.shadow.min.css"; 4 | @import "open-props/gray-hsl.shadow.min.css"; 5 | 6 | :host { 7 | --theme-bg: white; 8 | --theme-bd: hsl(0 0% 100% / var(--theme-bd-opacity)); 9 | --theme-bd-2: hsl(0 0% 100% / var(--theme-bd-2-opacity)); 10 | --theme-bd-opacity: 1; 11 | --theme-bd-2-opacity: 1; 12 | --theme-color: hotpink; 13 | --theme-blue: hsl(188 90% 45%); 14 | --theme-purple: hsl(267 100% 58%); 15 | 16 | --neon-pink: color(display-p3 1 0 1); 17 | --neon-purple: color(display-p3 .5 0 1); 18 | --neon-lime: color(display-p3 0.25 1 0); 19 | --neon-cyan: color(display-p3 0 1 1); 20 | 21 | --theme-text_color: var(--gray-11); 22 | --theme-icon_color: var(--gray-9); 23 | --theme-icon_hover-bg: white; 24 | --theme-icon_active-bg: white; 25 | 26 | --layer-top: 2147483647; 27 | --layer-1: 2147483646; 28 | --layer-2: 2147483645; 29 | --layer-3: 2147483644; 30 | --layer-4: 2147483643; 31 | --layer-5: 2147483642; 32 | 33 | --text-shadow: 0 1px hsl(0 0% 0% / 40%); 34 | 35 | @media (-webkit-min-device-pixel-ratio: 2) { 36 | --text-shadow: 0 .5px hsl(0 0% 0% / 50%); 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | --theme-bg: var(--gray-10); 41 | --theme-bd: hsl(var(--gray-10-hsl) / var(--theme-bd-opacity)); 42 | --theme-bd-2: hsl(var(--gray-10-hsl) / var(--theme-bd-2-opacity)); 43 | --theme-color: hsl(330deg 65% 75%); 44 | --theme-text_color: var(--gray-0); 45 | --theme-icon_color: var(--gray-2); 46 | --theme-icon_hover-bg: var(--gray-8); 47 | --theme-icon_active-bg: var(--gray-8); 48 | } 49 | 50 | @supports (backdrop-filter: blur(5px)) { 51 | --theme-bd-opacity: .8; 52 | --theme-bd-2-opacity: .9; 53 | } 54 | 55 | @supports (-webkit-backdrop-filter: blur(5px)) { 56 | --theme-bd-opacity: .8; 57 | --theme-bd-2-opacity: .9; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/components/_variables_dark.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --theme-bg: hsl(0 0% 10%); 3 | --theme-bd: hsl(0 0% 10% / var(--theme-bd-opacity)); 4 | --theme-bd-2: hsl(0 0% 10% / var(--theme-bd-2-opacity)); 5 | --theme-color: hsl(330deg 65% 75%); 6 | --theme-text_color: hsl(0 0% 90%); 7 | --theme-icon_color: hsl(0 0% 80%); 8 | --theme-icon_hover-bg: hsl(0 0% 15%); 9 | --theme-icon_active-bg: hsl(0 0% 20%); 10 | } 11 | -------------------------------------------------------------------------------- /app/components/_variables_light.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --theme-bg: hsl(0 0% 100%); 3 | --theme-bd: hsl(0 0% 100% / var(--theme-bd-opacity)); 4 | --theme-bd-2: hsl(0 0% 100% / var(--theme-bd-2-opacity)); 5 | --theme-color: hotpink; 6 | --theme-text_color: hsl(0 0% 10%); 7 | --theme-icon_color: hsl(0 0% 20%); 8 | --theme-icon_hover-bg: hsl(0 0% 95%); 9 | --theme-icon_active-bg: hsl(0 0% 90%); 10 | } 11 | -------------------------------------------------------------------------------- /app/components/hotkey-map/accessibility.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { accessibility as icon } from '../vis-bug/vis-bug.icons' 3 | import { altKey } from '../../utilities'; 4 | 5 | export class AccessibilityHotkeys extends HotkeyMap { 6 | constructor() { 7 | super() 8 | 9 | this._hotkey = 'p' 10 | this._usedkeys = [altKey] 11 | this.tool = 'accessibility' 12 | } 13 | 14 | show() { 15 | this.$shadow.host.style.display = 'flex' 16 | } 17 | 18 | render() { 19 | return ` 20 |
21 |
22 | 23 | ${icon} 24 | ${this._tool} Tool 25 | 26 |
27 |
28 | coming soon 29 |
30 |
31 | ` 32 | } 33 | } 34 | 35 | customElements.define('hotkeys-accessibility', AccessibilityHotkeys) 36 | -------------------------------------------------------------------------------- /app/components/hotkey-map/align.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { metaKey } from '../../utilities'; 3 | 4 | const h_alignOptions = ['left','center','right'] 5 | const v_alignOptions = ['top','center','bottom'] 6 | const distOptions = ['evenly','normal','between'] 7 | 8 | export class AlignHotkeys extends HotkeyMap { 9 | constructor() { 10 | super() 11 | 12 | this._hotkey = 'a' 13 | this._usedkeys = [metaKey,'shift'] 14 | 15 | this._htool = 0 16 | this._vtool = 0 17 | this._dtool = 1 18 | 19 | this._side = 'top left' 20 | this._direction = 'row' 21 | this._distribution = distOptions[this._dtool] 22 | this._wrap = 'no wrap' 23 | 24 | this.tool = 'align' 25 | } 26 | 27 | createCommand({e:{code}, hotkeys}) { 28 | let amount = this._distribution 29 | , negative_modifier = this._direction 30 | , side = this._side 31 | , negative = this._wrap 32 | 33 | if (hotkeys[metaKey] && hotkeys.shift) { 34 | if (code === 'ArrowUp') 35 | negative = 'no wrap' 36 | else if (code === 'ArrowDown') 37 | negative = 'wrap' 38 | else if (code === 'ArrowLeft') 39 | negative_modifier = `${negative_modifier}-reverse` 40 | } 41 | else if (hotkeys[metaKey] && (code === 'ArrowRight' || code === 'ArrowDown')) { 42 | negative_modifier = code === 'ArrowDown' 43 | ? 'column' 44 | : 'row' 45 | this._direction = negative_modifier 46 | } 47 | else { 48 | if (code === 'ArrowUp') side = this.clamp(v_alignOptions, '_vtool') 49 | else if (code === 'ArrowDown') side = this.clamp(v_alignOptions, '_vtool', true) 50 | else side = v_alignOptions[this._vtool] 51 | 52 | if (code === 'ArrowLeft') side += ' ' + this.clamp(h_alignOptions, '_htool') 53 | else if (code === 'ArrowRight') side += ' ' + this.clamp(h_alignOptions, '_htool', true) 54 | else side += ' ' + h_alignOptions[this._htool] 55 | 56 | this._side = side 57 | 58 | if (hotkeys.shift && (code === 'ArrowRight' || code === 'ArrowLeft')) { 59 | amount = this.clamp(distOptions, '_dtool', code === 'ArrowRight') 60 | this._distribution = amount 61 | } 62 | } 63 | 64 | return { 65 | negative, negative_modifier, amount, side, 66 | } 67 | } 68 | 69 | displayCommand({side, amount, negative, negative_modifier}) { 70 | if (amount == 1) amount = this._distribution 71 | if (negative_modifier == ' to ') negative_modifier = this._direction 72 | 73 | return ` 74 | ${this._tool} 75 | as 76 | ${negative_modifier}: 77 | ${side} 78 | , distributed 79 | ${amount}, 80 | with 81 | ${negative} 82 | ` 83 | } 84 | 85 | clamp(range, tool, increment = false) { 86 | if (increment) { 87 | if (this[tool] < range.length - 1) 88 | this[tool] = this[tool] + 1 89 | } 90 | else if (this[tool] > 0) 91 | this[tool] = this[tool] - 1 92 | 93 | return range[this[tool]] 94 | } 95 | } 96 | 97 | customElements.define('hotkeys-align', AlignHotkeys) 98 | -------------------------------------------------------------------------------- /app/components/hotkey-map/base.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | display: none; 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: var(--layer-top); 9 | align-items: center; 10 | justify-content: center; 11 | width: 100vw; 12 | height: 100vh; 13 | background: var(--theme-bd-2); 14 | backdrop-filter: blur(5px); 15 | font-size: 16px; 16 | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; 17 | cursor: initial; 18 | 19 | --light-grey: hsl(0 0% 90%); 20 | --grey: hsl(0 0% 60%); 21 | --dark-grey: hsl(0 0% 40%); 22 | 23 | @media (prefers-color-scheme: dark) { 24 | --light-grey: hsl(0 0% 20%); 25 | --grey: hsl(0 0% 60%); 26 | --dark-grey: hsl(0 0% 80%); 27 | } 28 | } 29 | 30 | :host [command] { 31 | padding: 1em; 32 | text-align: center; 33 | font-size: 3vw; 34 | font-weight: lighter; 35 | letter-spacing: 0.1em; 36 | color: var(--dark-grey); 37 | 38 | & > [light] { 39 | color: var(--grey); 40 | } 41 | 42 | & > [tool] { 43 | text-decoration: underline; 44 | text-decoration-color: var(--theme-color); 45 | } 46 | 47 | & > :matches([negative],[side],[amount]) { 48 | font-weight: normal; 49 | } 50 | } 51 | 52 | :host [card] { 53 | padding: 1em; 54 | background: var(--theme-bg); 55 | border-radius: 0.5em; 56 | color: var(--dark-grey); 57 | display: flex; 58 | justify-content: space-evenly; 59 | 60 | & > div:not([keyboard]) { 61 | display: flex; 62 | align-items: flex-end; 63 | margin-left: 1em; 64 | } 65 | } 66 | 67 | :host [tool-icon] { 68 | position: absolute; 69 | top: 1em; 70 | left: 0; 71 | width: 100%; 72 | padding: 0 1rem; 73 | display: flex; 74 | justify-content: center; 75 | 76 | & > span { 77 | color: var(--dark-grey); 78 | display: grid; 79 | grid-template-columns: 5vmax auto; 80 | grid-gap: 0.5em; 81 | align-items: center; 82 | text-transform: capitalize; 83 | font-size: 4vmax; 84 | font-weight: lighter; 85 | } 86 | 87 | & svg { 88 | width: 100%; 89 | fill: var(--theme-color); 90 | } 91 | } 92 | 93 | :host section { 94 | display: flex; 95 | justify-content: center; 96 | } 97 | 98 | :host section > span, 99 | :host [arrows] > span { 100 | border: 1px solid transparent; 101 | border-radius: 0.5em; 102 | display: inline-flex; 103 | align-items: center; 104 | justify-content: center; 105 | margin: 2px; 106 | padding: 1.5vw; 107 | font-size: 0.75em; 108 | white-space: nowrap; 109 | } 110 | 111 | :host section > span:not([pressed="true"]), 112 | :host [arrows] > span:not([pressed="true"]) { 113 | border: 1px solid var(--light-grey); 114 | 115 | &:hover { 116 | border-color: var(--grey); 117 | } 118 | } 119 | 120 | :host span[pressed="true"] { 121 | background: var(--theme-color); 122 | color: var(--theme-bg); 123 | } 124 | 125 | :host span:not([pressed="true"]):matches([used]) { 126 | background: var(--light-grey); 127 | cursor: pointer; 128 | } 129 | 130 | :host span[hotkey] { 131 | color: var(--theme-color); 132 | font-weight: bold; 133 | cursor: pointer; 134 | } 135 | 136 | :host section > span[hotkey]:not([pressed="true"]) { 137 | border-color: var(--theme-color); 138 | } 139 | 140 | :host [arrows] { 141 | display: grid; 142 | grid-template-columns: 1fr 1fr 1fr; 143 | grid-template-rows: 1fr 1fr; 144 | 145 | & > span:nth-child(1) { 146 | grid-row: 1; 147 | grid-column: 2; 148 | } 149 | 150 | & > span:nth-child(2) { 151 | grid-row: 2; 152 | grid-column: 2; 153 | } 154 | 155 | & > span:nth-child(3) { 156 | grid-row: 2; 157 | grid-column: 1; 158 | } 159 | 160 | & > span:nth-child(4) { 161 | grid-row: 2; 162 | grid-column: 3; 163 | } 164 | } 165 | 166 | :host [caps] > span:nth-child(1), 167 | :host [shift] > span:nth-child(1) { justify-content: flex-start; } 168 | :host [shift] > span:nth-child(12) { justify-content: flex-end; } -------------------------------------------------------------------------------- /app/components/hotkey-map/base.element_dark.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --light-grey: hsl(0 0% 20%); 3 | --grey: hsl(0 0% 60%); 4 | --dark-grey: hsl(0 0% 80%); 5 | } 6 | -------------------------------------------------------------------------------- /app/components/hotkey-map/base.element_light.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --light-grey: hsl(0 0% 90%); 3 | --grey: hsl(0 0% 60%); 4 | --dark-grey: hsl(0 0% 40%); 5 | } 6 | -------------------------------------------------------------------------------- /app/components/hotkey-map/boxshadow.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { boxshadow as icon } from '../vis-bug/vis-bug.icons' 3 | import { metaKey } from '../../utilities'; 4 | 5 | export class BoxshadowHotkeys extends HotkeyMap { 6 | constructor() { 7 | super() 8 | 9 | this._hotkey = 'd' 10 | this._usedkeys = ['shift',metaKey] 11 | this.tool = 'boxshadow' 12 | } 13 | 14 | show() { 15 | this.$shadow.host.style.display = 'flex' 16 | } 17 | 18 | render() { 19 | return ` 20 |
21 |
22 | 23 | ${icon} 24 | ${this._tool} Tool 25 | 26 |
27 |
28 | coming soon 29 |
30 |
31 | ` 32 | } 33 | } 34 | 35 | customElements.define('hotkeys-boxshadow', BoxshadowHotkeys) 36 | -------------------------------------------------------------------------------- /app/components/hotkey-map/font.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { metaKey, altKey } from '../../utilities'; 3 | 4 | export class FontHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 'f' 9 | this._usedkeys = ['shift',metaKey] 10 | this.tool = 'font' 11 | } 12 | 13 | createCommand({e:{code}, hotkeys}) { 14 | let amount = hotkeys.shift ? 10 : 1 15 | let negative = '[increase/decrease]' 16 | let negative_modifier = 'by' 17 | let side = '[arrow key]' 18 | 19 | // kerning 20 | if (hotkeys.shift && (code === 'ArrowLeft' || code === 'ArrowRight')) { 21 | side = 'kerning' 22 | amount = '1px' 23 | 24 | if (code === 'ArrowLeft') 25 | negative = 'decrease' 26 | if (code === 'ArrowRight') 27 | negative = 'increase' 28 | } 29 | // leading 30 | else if (hotkeys.shift && (code === 'ArrowUp' || code === 'ArrowDown')) { 31 | side = 'leading' 32 | amount = '1px' 33 | 34 | if (code === 'ArrowUp') 35 | negative = 'increase' 36 | if (code === 'ArrowDown') 37 | negative = 'decrease' 38 | } 39 | // font weight 40 | else if (hotkeys[metaKey] && (code === 'ArrowUp' || code === 'ArrowDown')) { 41 | side = 'font weight' 42 | amount = '' 43 | negative_modifier = '' 44 | 45 | if (code === 'ArrowUp') 46 | negative = 'increase' 47 | if (code === 'ArrowDown') 48 | negative = 'decrease' 49 | } 50 | // font size 51 | else if (code === 'ArrowUp' || code === 'ArrowDown') { 52 | side = 'font size' 53 | amount = '1px' 54 | 55 | if (code === 'ArrowUp') 56 | negative = 'increase' 57 | if (code === 'ArrowDown') 58 | negative = 'decrease' 59 | } 60 | // text alignment 61 | else if (code === 'ArrowRight' || code === 'ArrowLeft') { 62 | side = 'text alignment' 63 | amount = '' 64 | negative = 'adjust' 65 | negative_modifier = '' 66 | } 67 | 68 | return { 69 | negative, negative_modifier, amount, side, 70 | } 71 | } 72 | 73 | displayCommand({negative, negative_modifier, side, amount}) { 74 | if (negative === `±[${altKey}] `) 75 | negative = '[increase/decrease]' 76 | if (negative_modifier === ' to ') 77 | negative_modifier = ' by ' 78 | 79 | return ` 80 | ${negative} 81 | ${side} 82 | ${negative_modifier} 83 | ${amount} 84 | ` 85 | } 86 | } 87 | 88 | customElements.define('hotkeys-font', FontHotkeys) 89 | -------------------------------------------------------------------------------- /app/components/hotkey-map/guides.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { guides as icon } from '../vis-bug/vis-bug.icons' 3 | 4 | export class GuidesHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 'g' 9 | this._usedkeys = [] 10 | this.tool = 'guides' 11 | } 12 | 13 | show() { 14 | this.$shadow.host.style.display = 'flex' 15 | } 16 | 17 | render() { 18 | return ` 19 |
20 |
21 | 22 | ${icon} 23 | ${this._tool} Tool 24 | 25 |
26 |
27 | coming soon 28 |
29 |
30 | ` 31 | } 32 | } 33 | 34 | customElements.define('hotkeys-guides', GuidesHotkeys) 35 | -------------------------------------------------------------------------------- /app/components/hotkey-map/hotkeys.element.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import hotkeys from 'hotkeys-js' 3 | 4 | import { GuidesHotkeys } from './guides.element' 5 | import { InspectorHotkeys } from './inspector.element' 6 | import { AccessibilityHotkeys } from './accessibility.element' 7 | import { MoveHotkeys } from './move.element' 8 | import { MarginHotkeys } from './margin.element' 9 | import { PaddingHotkeys } from './padding.element' 10 | import { AlignHotkeys } from './align.element' 11 | import { HueshiftHotkeys } from './hueshift.element' 12 | import { BoxshadowHotkeys } from './boxshadow.element' 13 | import { PositionHotkeys } from './position.element' 14 | import { FontHotkeys } from './font.element' 15 | import { TextHotkeys } from './text.element' 16 | import { SearchHotkeys } from './search.element' 17 | 18 | export class Hotkeys extends HTMLElement { 19 | 20 | constructor() { 21 | super() 22 | 23 | this.tool_map = { 24 | guides: document.createElement('hotkeys-guides'), 25 | inspector: document.createElement('hotkeys-inspector'), 26 | accessibility: document.createElement('hotkeys-accessibility'), 27 | move: document.createElement('hotkeys-move'), 28 | margin: document.createElement('hotkeys-margin'), 29 | padding: document.createElement('hotkeys-padding'), 30 | align: document.createElement('hotkeys-align'), 31 | hueshift: document.createElement('hotkeys-hueshift'), 32 | boxshadow: document.createElement('hotkeys-boxshadow'), 33 | position: document.createElement('hotkeys-position'), 34 | font: document.createElement('hotkeys-font'), 35 | text: document.createElement('hotkeys-text'), 36 | search: document.createElement('hotkeys-search'), 37 | } 38 | 39 | Object.values(this.tool_map).forEach(tool => 40 | this.appendChild(tool)) 41 | } 42 | 43 | connectedCallback() { 44 | hotkeys('shift+/', e => 45 | this.cur_tool 46 | ? this.hideTool() 47 | : this.showTool()) 48 | 49 | hotkeys('esc', e => this.hideTool()) 50 | } 51 | 52 | disconnectedCallback() {} 53 | 54 | hideTool() { 55 | if (!this.cur_tool) return 56 | this.cur_tool.hide() 57 | this.cur_tool = null 58 | } 59 | 60 | showTool() { 61 | this.cur_tool = this.tool_map[ 62 | $('vis-bug')[0].activeTool] 63 | this.cur_tool.show() 64 | } 65 | } 66 | 67 | customElements.define('visbug-hotkeys', Hotkeys) 68 | -------------------------------------------------------------------------------- /app/components/hotkey-map/hueshift.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { hueshift as icon } from '../vis-bug/vis-bug.icons' 3 | import { metaKey, altKey } from '../../utilities'; 4 | 5 | export class HueshiftHotkeys extends HotkeyMap { 6 | constructor() { 7 | super() 8 | 9 | this._hotkey = 'h' 10 | this._usedkeys = ['shift',metaKey] 11 | this.tool = 'hueshift' 12 | } 13 | 14 | createCommand({e:{code}, hotkeys}) { 15 | let amount = hotkeys.shift ? 10 : 1 16 | let negative = '[increase/decrease]' 17 | let negative_modifier = 'by' 18 | let side = '[arrow key]' 19 | 20 | // saturation 21 | if (hotkeys[metaKey]) { 22 | side ='hue' 23 | 24 | if (code === 'ArrowDown') 25 | negative = 'decrease' 26 | if (code === 'ArrowUp') 27 | negative = 'increase' 28 | } 29 | else if (code === 'ArrowLeft' || code === 'ArrowRight') { 30 | side = 'saturation' 31 | 32 | if (code === 'ArrowLeft') 33 | negative = 'decrease' 34 | if (code === 'ArrowRight') 35 | negative = 'increase' 36 | } 37 | // lightness 38 | else if (code === 'ArrowUp' || code === 'ArrowDown') { 39 | side = 'lightness' 40 | 41 | if (code === 'ArrowDown') 42 | negative = 'decrease' 43 | if (code === 'ArrowUp') 44 | negative = 'increase' 45 | } 46 | 47 | return { 48 | negative, negative_modifier, amount, side, 49 | } 50 | } 51 | 52 | displayCommand({negative, negative_modifier, side, amount}) { 53 | if (negative === `±[${altKey}] `) 54 | negative = '[increase/decrease]' 55 | if (negative_modifier === ' to ') 56 | negative_modifier = ' by ' 57 | 58 | return ` 59 | ${negative} 60 | ${side} 61 | ${negative_modifier} 62 | ${amount} 63 | ` 64 | } 65 | } 66 | 67 | customElements.define('hotkeys-hueshift', HueshiftHotkeys) 68 | -------------------------------------------------------------------------------- /app/components/hotkey-map/inspector.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { inspector as icon } from '../vis-bug/vis-bug.icons' 3 | import { altKey } from '../../utilities'; 4 | 5 | export class InspectorHotkeys extends HotkeyMap { 6 | constructor() { 7 | super() 8 | 9 | this._hotkey = 'i' 10 | this._usedkeys = [altKey] 11 | this.tool = 'inspector' 12 | } 13 | 14 | show() { 15 | this.$shadow.host.style.display = 'flex' 16 | } 17 | 18 | render() { 19 | return ` 20 |
21 |
22 | 23 | ${icon} 24 | ${this._tool} Tool 25 | 26 |
27 |
28 | coming soon 29 |
30 |
31 | ` 32 | } 33 | } 34 | 35 | customElements.define('hotkeys-inspector', InspectorHotkeys) 36 | -------------------------------------------------------------------------------- /app/components/hotkey-map/margin.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { metaKey, altKey } from '../../utilities/' 3 | 4 | export class MarginHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 'm' 9 | this._usedkeys = ['shift',metaKey,altKey] 10 | 11 | this.tool = 'margin' 12 | } 13 | } 14 | 15 | customElements.define('hotkeys-margin', MarginHotkeys) 16 | -------------------------------------------------------------------------------- /app/components/hotkey-map/move.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | 3 | export class MoveHotkeys extends HotkeyMap { 4 | constructor() { 5 | super() 6 | 7 | this._hotkey = 'v' 8 | this.tool = 'move' 9 | } 10 | 11 | createCommand({e:{code}, hotkeys}) { 12 | let amount, negative, negative_modifier 13 | 14 | let side = '[arrow key]' 15 | if (code === 'ArrowUp') side = 'up & out of div' 16 | if (code === 'ArrowDown') side = 'down & into next sibling / out & under div' 17 | if (code === 'ArrowLeft') side = 'towards the front/top of the stack' 18 | if (code === 'ArrowRight') side = 'towards the back/bottom of the stack' 19 | 20 | return { 21 | negative, negative_modifier, amount, side, 22 | } 23 | } 24 | 25 | displayCommand({side}) { 26 | return ` 27 | ${this._tool} 28 | ${side} 29 | ` 30 | } 31 | } 32 | 33 | customElements.define('hotkeys-move', MoveHotkeys) -------------------------------------------------------------------------------- /app/components/hotkey-map/padding.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { metaKey, altKey } from '../../utilities/' 3 | 4 | export class PaddingHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 'p' 9 | this._usedkeys = ['shift',metaKey,altKey] 10 | 11 | this.tool = 'padding' 12 | } 13 | } 14 | 15 | customElements.define('hotkeys-padding', PaddingHotkeys) 16 | -------------------------------------------------------------------------------- /app/components/hotkey-map/position.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { position as icon } from '../vis-bug/vis-bug.icons' 3 | import { altKey } from '../../utilities'; 4 | 5 | export class PositionHotkeys extends HotkeyMap { 6 | constructor() { 7 | super() 8 | 9 | this._hotkey = 'l' 10 | this._usedkeys = ['shift',altKey] 11 | this.tool = 'position' 12 | } 13 | 14 | show() { 15 | this.$shadow.host.style.display = 'flex' 16 | } 17 | 18 | render() { 19 | return ` 20 |
21 |
22 | 23 | ${icon} 24 | ${this._tool} Tool 25 | 26 |
27 |
28 | coming soon 29 |
30 |
31 | ` 32 | } 33 | } 34 | 35 | customElements.define('hotkeys-position', PositionHotkeys) 36 | -------------------------------------------------------------------------------- /app/components/hotkey-map/search.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { search as icon } from '../vis-bug/vis-bug.icons' 3 | 4 | export class SearchHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 's' 9 | this._usedkeys = [] 10 | this.tool = 'search' 11 | } 12 | 13 | show() { 14 | this.$shadow.host.style.display = 'flex' 15 | } 16 | 17 | render() { 18 | return ` 19 |
20 |
21 | 22 | ${icon} 23 | ${this._tool} Tool 24 | 25 |
26 |
27 | coming soon 28 |
29 |
30 | ` 31 | } 32 | } 33 | 34 | customElements.define('hotkeys-search', SearchHotkeys) 35 | -------------------------------------------------------------------------------- /app/components/hotkey-map/text.element.js: -------------------------------------------------------------------------------- 1 | import { HotkeyMap } from './base.element' 2 | import { text as icon } from '../vis-bug/vis-bug.icons' 3 | 4 | export class TextHotkeys extends HotkeyMap { 5 | constructor() { 6 | super() 7 | 8 | this._hotkey = 'e' 9 | this._usedkeys = [] 10 | this.tool = 'text' 11 | } 12 | 13 | show() { 14 | this.$shadow.host.style.display = 'flex' 15 | } 16 | 17 | render() { 18 | return ` 19 |
20 |
21 | 22 | ${icon} 23 | ${this._tool} Tool 24 | 25 |
26 |
27 | coming soon 28 |
29 |
30 | ` 31 | } 32 | } 33 | 34 | customElements.define('hotkeys-text', TextHotkeys) 35 | -------------------------------------------------------------------------------- /app/components/index.js: -------------------------------------------------------------------------------- 1 | export { Handles } from './selection/handles.element' 2 | export { Handle } from './selection/handle.element' 3 | export { Hover } from './selection/hover.element' 4 | export { Label } from './selection/label.element' 5 | export { Gridlines } from './selection/gridlines.element' 6 | export { Distance } from './selection/distance.element' 7 | export { Overlay } from './selection/overlay.element' 8 | export { BoxModel } from './selection/box-model.element' 9 | export { Corners } from './selection/corners.element' 10 | export { Grip } from './selection/grip.element' 11 | 12 | export { Metatip } from './metatip/metatip.element' 13 | export { Ally } from './metatip/ally.element' 14 | 15 | export { Hotkeys } from './hotkey-map/hotkeys.element' 16 | -------------------------------------------------------------------------------- /app/components/metatip/ally.element.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { Metatip } from './metatip.element.js' 3 | import { preferredNotation } from '../../features/color.js' 4 | import { draggable } from '../../features/' 5 | import { getStyle, getComputedBackgroundColor } from '../../utilities' 6 | import { contrast_color } from '../../utilities' 7 | 8 | export class Ally extends Metatip { 9 | constructor() { 10 | super() 11 | this.copyColorSwatch = this.copyColorSwatch.bind(this) 12 | } 13 | 14 | async copyToClipboard(text) { 15 | const {state} = await navigator.permissions.query({name:'clipboard-write'}) 16 | 17 | if (state === 'granted') 18 | navigator.clipboard.writeText(text) 19 | } 20 | 21 | copyColorSwatch(event) { 22 | this.copyToClipboard(event.currentTarget.querySelector('span').innerText.trim()) 23 | } 24 | 25 | observe() { 26 | $('[color-swatch]', this.$shadow).on('click', this.copyColorSwatch) 27 | 28 | draggable({ 29 | el: this, 30 | surface: this.$shadow.querySelector('header'), 31 | cursor: 'grab', 32 | }) 33 | } 34 | 35 | unobserve() { 36 | $('[color-swatch]', this.$shadow).off('click', this.copyColorSwatch) 37 | } 38 | 39 | render({el, ally_attributes, contrast_results}) { 40 | const colormode = $('vis-bug').attr('color-mode') 41 | 42 | const foreground = el instanceof SVGElement 43 | ? (getStyle(el, 'fill') || getStyle(el, 'stroke')) 44 | : getStyle(el, 'color') 45 | const background = getComputedBackgroundColor(el) 46 | 47 | const contrastingForegroundColor = contrast_color(foreground) 48 | const contrastingBackgroundColor = contrast_color(background) 49 | 50 | this.style.setProperty('--copy-message-left-color', contrastingForegroundColor) 51 | this.style.setProperty('--copy-message-right-color', contrastingBackgroundColor) 52 | 53 | return ` 54 |
55 |
56 |
<${el.nodeName.toLowerCase()}>${el.id && '#' + el.id}
57 |
58 |
59 |
60 | 61 | 62 | Foreground 63 | 64 | 65 | ${preferredNotation(foreground, colormode)} 66 | 67 | 68 | 69 | 70 | Background 71 | 72 | 73 | ${preferredNotation(background, colormode)} 74 | 75 | 76 |
77 | ${contrast_results} 78 |
79 | ${ally_attributes.length > 0 80 | ? ` 81 |
82 | ${ally_attributes.reduce((items, attr) => ` 83 | ${items} 84 | ${attr.prop}: 85 | ${attr.value} 86 | `, '')} 87 |
88 |
` 89 | : '' 90 | } 91 | 92 |
93 | ` 94 | } 95 | } 96 | 97 | customElements.define('visbug-ally', Ally) 98 | -------------------------------------------------------------------------------- /app/components/metatip/metatip.element.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { createClassname, schemeRule } from '../../utilities/' 3 | import { draggable } from '../../features/' 4 | import { MetatipStyles, MetatipLightStyles, MetatipDarkStyles } from '../styles.store' 5 | 6 | export class Metatip extends HTMLElement { 7 | 8 | constructor() { 9 | super() 10 | this.$shadow = this.attachShadow({mode: 'closed'}) 11 | this.applyScheme = schemeRule( 12 | this.$shadow, 13 | MetatipStyles, MetatipLightStyles, MetatipDarkStyles 14 | ) 15 | 16 | this.observe = this.observe.bind(this) 17 | this.dispatchQuery = this.dispatchQuery.bind(this) 18 | this.dispatchUnQuery = this.dispatchUnQuery.bind(this) 19 | } 20 | 21 | connectedCallback() { 22 | this.setAttribute('popover', 'manual') 23 | this.showPopover && this.showPopover() 24 | this.applyScheme(document.querySelector("vis-bug").getAttribute("color-scheme")) 25 | $(this.$shadow.host).on('mouseenter', this.observe) 26 | } 27 | 28 | disconnectedCallback() { 29 | this.unobserve() 30 | this.hidePopover && this.hidePopover() 31 | } 32 | 33 | dispatchQuery(e) { 34 | this.$shadow.host.dispatchEvent(new CustomEvent('query', { 35 | bubbles: true, 36 | detail: { 37 | text: e.target.textContent, 38 | activator: e.type, 39 | } 40 | })) 41 | } 42 | 43 | observe() { 44 | $('h5 > a', this.$shadow).on('click mouseenter', this.dispatchQuery) 45 | $('h5 > a', this.$shadow).on('mouseleave', this.dispatchUnQuery) 46 | 47 | draggable({ 48 | el: this, 49 | surface: this.$shadow.querySelector('header'), 50 | cursor: 'grab', 51 | }) 52 | } 53 | 54 | unobserve() { 55 | $('h5 > a', this.$shadow).off('click mouseenter', this.dispatchQuery) 56 | $('h5 > a', this.$shadow).off('mouseleave', this.dispatchUnQuery) 57 | } 58 | 59 | dispatchUnQuery(e) { 60 | this.$shadow.host.dispatchEvent(new CustomEvent('unquery', { 61 | bubbles: true 62 | })) 63 | this.unobserve() 64 | this.teardown() 65 | } 66 | 67 | set meta(data) { 68 | this.$shadow.innerHTML = this.render(data) 69 | } 70 | 71 | render({el, width, height, localModifications, notLocalModifications}) { 72 | return ` 73 |
74 |
75 |
76 | ${el.nodeName.toLowerCase()} 77 | ${el.id && '#' + el.id} 78 | ${createClassname(el).split('.') 79 | .filter(name => name != '') 80 | .reduce((links, name) => ` 81 | ${links} 82 | .${name} 83 | `, '') 84 | } 85 |
86 | 87 | ${Math.round(width)}px 88 | × 89 | ${Math.round(height)}px 90 | 91 |
92 | 93 | ${notLocalModifications.reduce((items, item) => ` 94 | ${items} 95 | ${item.prop}: 96 | ${item.value} 97 | `, '')} 98 | 99 | ${localModifications.length ? ` 100 |
101 | Local Modifications / Inline Styles 102 | ${localModifications.reduce((items, item) => ` 103 | ${items} 104 | ${item.prop}: 105 | ${item.value} 106 | `, '')} 107 | 108 |
109 | ` : ''} 110 |
111 | ` 112 | } 113 | } 114 | 115 | customElements.define('visbug-metatip', Metatip) 116 | -------------------------------------------------------------------------------- /app/components/metatip/metatip.element_dark.css: -------------------------------------------------------------------------------- 1 | :host { 2 | & [pass="true"] { color: hsl(120deg 50% 75%); } 3 | & [pass="false"] { color: hsl(0deg 50% 65%); } 4 | } 5 | -------------------------------------------------------------------------------- /app/components/metatip/metatip.element_light.css: -------------------------------------------------------------------------------- 1 | :host { 2 | & [pass="true"] { color: green; } 3 | & [pass="false"] { color: red; } 4 | } 5 | -------------------------------------------------------------------------------- /app/components/selection/box-model.element.css: -------------------------------------------------------------------------------- 1 | :host [mask] { 2 | pointer-events: none; 3 | position: absolute; 4 | z-index: var(--layer-5); 5 | width: var(--width); 6 | height: var(--height); 7 | top: var(--top); 8 | left: var(--left); 9 | background-color: var(--bg); 10 | clip-path: polygon( 11 | 0% 0%, 0% 100%, var(--target-left) 100%, 12 | var(--target-left) var(--target-top), 13 | var(--offset-right) var(--target-top), 14 | var(--offset-right) var(--offset-bottom), 15 | 0 var(--offset-bottom), 0 100%, 16 | 100% 100%, 100% 0% 17 | ); 18 | } -------------------------------------------------------------------------------- /app/components/selection/corners.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host rect { 4 | width: 5px; 5 | height: 5px; 6 | vector-effect: non-scaling-stroke; 7 | stroke: var(--neon-purple); 8 | stroke-width: 1px; 9 | fill: none; 10 | stroke-linecap: round; 11 | stroke-dasharray: 10px; 12 | stroke-dashoffset: 5px; 13 | x: 1px; 14 | y: 1px; 15 | 16 | &:nth-child(2) { 17 | x: calc(100% - 6px); 18 | y: 1px; 19 | stroke-dashoffset: 0; 20 | } 21 | 22 | &:nth-child(3) { 23 | x: calc(100% - 6px); 24 | y: calc(100% - 6px); 25 | stroke-dashoffset: 15px; 26 | } 27 | 28 | &:nth-child(4) { 29 | x: 1px; 30 | y: calc(100% - 6px); 31 | stroke-dashoffset: 10px; 32 | } 33 | } 34 | 35 | :host > svg { 36 | z-index: var(--layer-5); 37 | } 38 | -------------------------------------------------------------------------------- /app/components/selection/corners.element.js: -------------------------------------------------------------------------------- 1 | import { Handles } from './handles.element' 2 | import { HandlesStyles, CornerStyles } from '../styles.store' 3 | 4 | export class Corners extends Handles { 5 | 6 | constructor() { 7 | super() 8 | this.styles = [HandlesStyles, CornerStyles] 9 | } 10 | 11 | render({ width, height, top, left }) { 12 | this.style.setProperty('--top', `${top + window.scrollY}px`) 13 | this.style.setProperty('--left', `${left}px`) 14 | 15 | return ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | ` 23 | } 24 | } 25 | 26 | customElements.define('visbug-corners', Corners) 27 | -------------------------------------------------------------------------------- /app/components/selection/distance.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | --line-color: 1 0 1; 5 | --line-base: .75 0 1; 6 | --line-width: 1px; 7 | --distance-h: 5px; 8 | --distance-w: 5px; 9 | --line-w: 1px; 10 | --line-h: 1px; 11 | font-size: 16px; 12 | position: initial; 13 | background: transparent; 14 | border: none; 15 | overflow: visible; 16 | padding: 0; 17 | margin: 0; 18 | } 19 | 20 | :host > figure { 21 | margin: 0; 22 | position: absolute; 23 | height: var(--distance-h); 24 | width: var(--distance-w); 25 | inset: var(--top) var(--right) auto var(--left); 26 | overflow: visible; 27 | pointer-events: none; 28 | z-index: var(--layer-3); 29 | display: flex; 30 | align-items: center; 31 | justify-content: var(--justify, 'flex-start'); 32 | flex-direction: var(--direction); 33 | } 34 | 35 | :host > figure figcaption { 36 | min-height: 3ex; 37 | width: max-content; 38 | display: inline-flex; 39 | align-items: center; 40 | justify-content: center; 41 | color: white; 42 | text-shadow: var(--text-shadow); 43 | box-shadow: var(--text-shadow); 44 | background: color(display-p3 var(--line-color) / 75%); 45 | border: 1px solid color(display-p3 var(--line-color)); 46 | border-radius: 1em; 47 | font-size: 0.7em; 48 | font-weight: bold; 49 | line-height: 1.1; 50 | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; 51 | padding: 0 1ex; 52 | } 53 | 54 | :host > figure span { 55 | background: color(display-p3 var(--line-color)); 56 | height: var(--line-h); 57 | width: var(--line-w); 58 | } 59 | 60 | :host > figure div { 61 | flex: 2; 62 | background: color(display-p3 var(--line-color)); 63 | width: var(--line-w); 64 | height: var(--line-h); 65 | } 66 | 67 | :host figure:matches([quadrant="bottom"], [quadrant="right"]) > div:first-of-type, 68 | :host figure:matches([quadrant="top"], [quadrant="left"]) > div:last-of-type { 69 | background: linear-gradient(to var(--quadrant), var(--neon-pink) 0%, color(display-p3 var(--line-color)) 100%); 70 | } 71 | 72 | :host::backdrop { 73 | background: none !important; 74 | } 75 | -------------------------------------------------------------------------------- /app/components/selection/distance.element.js: -------------------------------------------------------------------------------- 1 | import { DistanceStyles } from '../styles.store' 2 | 3 | export class Distance extends HTMLElement { 4 | 5 | constructor() { 6 | super() 7 | this.$shadow = this.attachShadow({mode: 'open'}) 8 | } 9 | 10 | connectedCallback() { 11 | this.$shadow.adoptedStyleSheets = [DistanceStyles] 12 | } 13 | 14 | disconnectedCallback() { 15 | if (this.hasAttribute('popover')) 16 | this.hidePopover && this.hidePopover() 17 | } 18 | 19 | set position({line_model, node_label_id}) { 20 | this.styleProps = line_model 21 | this.$shadow.innerHTML = this.render(line_model, node_label_id) 22 | } 23 | 24 | set styleProps({y,x,d,q,v = false, color}) { 25 | this.style.setProperty('--top', `${Math.round(y + window.scrollY)}px`) 26 | this.style.setProperty('--right', 'auto') 27 | this.style.setProperty('--left', `${x}px`) 28 | this.style.setProperty('--direction', v ? 'column' : 'row') 29 | this.style.setProperty('--quadrant', q) 30 | 31 | if (q === 'left') 32 | this.style.setProperty('--justify', 'flex-end') 33 | 34 | v 35 | ? this.style.setProperty('--distance-h', `${d}px`) 36 | : this.style.setProperty('--distance-w', `${d}px`) 37 | 38 | v 39 | ? this.style.setProperty('--line-h', `var(--line-w)`) 40 | : this.style.setProperty('--line-w', `var(--line-w)`) 41 | 42 | this.style.setProperty('--line-color', color === 'pink' 43 | ? '1 0 1' 44 | : '.5 0 1') 45 | this.style.setProperty('--line-base', color === 'pink' 46 | ? '1 0 1' 47 | : '.5 0 1') 48 | } 49 | 50 | render({q,d}, node_label_id) { 51 | this.$shadow.host.setAttribute('data-label-id', node_label_id) 52 | 53 | return ` 54 |
55 |
56 |
${Math.round(d)}
57 |
58 |
59 | ` 60 | } 61 | 62 | isPopover() { 63 | this.setAttribute('popover', 'manual') 64 | this.showPopover && this.showPopover() 65 | } 66 | } 67 | 68 | customElements.define('visbug-distance', Distance) 69 | -------------------------------------------------------------------------------- /app/components/selection/gridlines.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | position: fixed; 5 | background: transparent; 6 | pointer-events: none; 7 | border: none; 8 | overflow: visible; 9 | padding: 0; 10 | margin: 0; 11 | inset: 0; 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | :host > svg { 17 | position:fixed; 18 | top:0; 19 | left:0; 20 | overflow:visible; 21 | pointer-events:none; 22 | z-index:var(--layer-5); 23 | } 24 | 25 | :host rect, 26 | :host line { 27 | stroke: var(--neon-pink); 28 | } 29 | 30 | :host line { 31 | stroke-dasharray: 2; 32 | stroke-dasharray-offset: 3; 33 | } 34 | 35 | :host::backdrop { 36 | background: none !important; 37 | } 38 | -------------------------------------------------------------------------------- /app/components/selection/gridlines.element.js: -------------------------------------------------------------------------------- 1 | import { windowBounds } from '../../utilities/' 2 | import { GridlineStyles } from '../styles.store' 3 | 4 | export class Gridlines extends HTMLElement { 5 | 6 | constructor() { 7 | super() 8 | this.$shadow = this.attachShadow({mode: 'closed'}) 9 | } 10 | 11 | connectedCallback() { 12 | this.$shadow.adoptedStyleSheets = [GridlineStyles] 13 | this.setAttribute('popover', 'manual') 14 | this.showPopover && this.showPopover() 15 | } 16 | 17 | disconnectedCallback() { 18 | this.hidePopover && this.hidePopover() 19 | } 20 | 21 | set position(boundingRect) { 22 | this.$shadow.innerHTML = this.render(boundingRect) 23 | } 24 | 25 | set update({ width, height, top, left }) { 26 | const { winHeight, winWidth } = windowBounds() 27 | const svg = this.$shadow.querySelector('svg') 28 | const [rect,line1,line2,line3,line4] = svg.children 29 | 30 | this.$shadow.host.style.display = 'block' 31 | 32 | svg.setAttribute('viewBox', `0 0 ${winWidth} ${winHeight}`) 33 | rect.setAttribute('width', width + 'px') 34 | rect.setAttribute('x', left) 35 | rect.setAttribute('y', top) 36 | line1.setAttribute('x1', left) 37 | line1.setAttribute('x2', left) 38 | line2.setAttribute('x1', left + width) 39 | line2.setAttribute('x2', left + width) 40 | line3.setAttribute('y1', top) 41 | line3.setAttribute('y2', top) 42 | line3.setAttribute('x2', winWidth) 43 | line4.setAttribute('y1', top + height) 44 | line4.setAttribute('y2', top + height) 45 | line4.setAttribute('x2', winWidth) 46 | } 47 | 48 | render({ x, y, width, height, top, left }) { 49 | const { winWidth, winHeight } = windowBounds() 50 | 51 | return ` 52 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | ` 69 | } 70 | } 71 | 72 | customElements.define('visbug-gridlines', Gridlines) 73 | -------------------------------------------------------------------------------- /app/components/selection/grip.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host g { 4 | transform: translate(calc(50% - 10%), calc(50% - .25em)); 5 | } 6 | 7 | :host rect { 8 | vector-effect: non-scaling-stroke; 9 | height: .5em; 10 | width: 20%; 11 | max-width: 10vmax; 12 | stroke: var(--neon-pink); 13 | stroke-width: 1px; 14 | stroke-linecap: round; 15 | rx: 4; 16 | } 17 | 18 | :host > svg { 19 | z-index: var(--layer-5); 20 | 21 | &[hovering] rect { 22 | fill: var(--neon-pink); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/components/selection/grip.element.js: -------------------------------------------------------------------------------- 1 | import { Handles } from './handles.element' 2 | import { HandlesStyles, GripStyles } from '../styles.store' 3 | import { isFixed } from '../../utilities/'; 4 | 5 | export class Grip extends Handles { 6 | 7 | constructor() { 8 | super() 9 | this.styles = [HandlesStyles, GripStyles] 10 | } 11 | 12 | toggleHovering({hovering}) { 13 | hovering 14 | ? this.$shadow.children[0].setAttribute('hovering', true) 15 | : this.$shadow.children[0].removeAttribute('hovering') 16 | } 17 | 18 | render({ width, height, top, left }) { 19 | this.style.setProperty('--position', isFixed(this.$shadow.host) ? 'fixed' : 'absolute') 20 | this.style.setProperty('--top', `${top + window.scrollY}px`) 21 | this.style.setProperty('--left', `${left}px`) 22 | 23 | return ` 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ` 33 | } 34 | } 35 | 36 | customElements.define('visbug-grip', Grip) 37 | -------------------------------------------------------------------------------- /app/components/selection/handle.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | display: grid; 5 | grid-area: 1 / -1; 6 | place-self: var(--align-self, center) var(--justify-self, center); 7 | transform: translate(var(--translate-x, 0), var(--translate-y, 0)); 8 | } 9 | 10 | :host([hidden]) { 11 | display: none; 12 | } 13 | 14 | :host > button { 15 | pointer-events: auto; 16 | background-color: white; 17 | border: 1px solid var(--neon-pink); 18 | padding: 0; 19 | width: 0.5rem; 20 | height: 0.5rem; 21 | border-radius: 50%; 22 | position: relative; 23 | cursor: var(--cursor); 24 | 25 | /* increase tap target size */ 26 | &::before { 27 | content: ''; 28 | position: absolute; 29 | inset: -0.5rem; 30 | } 31 | } 32 | 33 | :host([placement^="top"]) { 34 | --align-self: start; 35 | --translate-y: -50%; 36 | } 37 | 38 | :host([placement^="bottom"]) { 39 | --align-self: end; 40 | --translate-y: 50%; 41 | } 42 | 43 | :host([placement$="start"]) { 44 | --justify-self: start; 45 | --translate-x: -50%; 46 | } 47 | 48 | :host([placement$="end"]) { 49 | --justify-self: end; 50 | --translate-x: 50%; 51 | } 52 | 53 | :host([placement^="top"]), 54 | :host([placement^="bottom"]) { 55 | --cursor: ns-resize; 56 | } 57 | 58 | :host([placement$="start"]), 59 | :host([placement$="end"]) { 60 | --cursor: ew-resize; 61 | } 62 | 63 | :host([placement="top-start"]) { 64 | --cursor: nw-resize; 65 | } 66 | 67 | :host([placement="top-end"]) { 68 | --cursor: ne-resize; 69 | } 70 | 71 | :host([placement="bottom-start"]) { 72 | --cursor: sw-resize; 73 | } 74 | 75 | :host([placement="bottom-end"]) { 76 | --cursor: se-resize; 77 | } 78 | -------------------------------------------------------------------------------- /app/components/selection/handles.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | position: var(--position, 'absolute'); 5 | inset: var(--top) auto auto var(--left); 6 | background: transparent; 7 | border: none; 8 | overflow: visible; 9 | padding: 0; 10 | pointer-events: none; 11 | z-index: var(--layer-3); 12 | width: var(--width); 13 | height: var(--height); 14 | display: grid; 15 | grid-template-rows: 1fr; 16 | isolation: isolate; 17 | } 18 | 19 | :host > svg { 20 | position: absolute; 21 | } 22 | 23 | :host::backdrop { 24 | background: none !important; 25 | } 26 | -------------------------------------------------------------------------------- /app/components/selection/handles.element.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { HandlesStyles } from '../styles.store' 3 | import { isFixed } from '../../utilities/'; 4 | 5 | export class Handles extends HTMLElement { 6 | 7 | constructor() { 8 | super() 9 | this.$shadow = this.attachShadow({mode: 'closed'}) 10 | this.styles = [HandlesStyles] 11 | this.on_resize = this.on_window_resize.bind(this) 12 | } 13 | 14 | connectedCallback() { 15 | this.$shadow.adoptedStyleSheets = this.styles 16 | this.setAttribute('popover', 'manual') 17 | this.showPopover && this.showPopover() 18 | window.addEventListener('resize', this.on_window_resize) 19 | } 20 | 21 | disconnectedCallback() { 22 | if (this.hidePopover && this.hidePopover()) this.hidePopover && this.hidePopover() 23 | window.removeEventListener('resize', this.on_window_resize) 24 | } 25 | 26 | on_window_resize() { 27 | if (!this.$shadow) return 28 | window.requestAnimationFrame(() => { 29 | const node_label_id = this.$shadow.host.getAttribute('data-label-id') 30 | const [source_el] = $(`[data-label-id="${node_label_id}"]`) 31 | 32 | if (!source_el) return 33 | 34 | this.position = { 35 | node_label_id, 36 | el: source_el, 37 | isFixed: isFixed(source_el), 38 | } 39 | }) 40 | } 41 | 42 | set position({el, node_label_id}) { 43 | this.$shadow.innerHTML = this.render(el.getBoundingClientRect(), node_label_id, isFixed(el)) 44 | 45 | if (this._backdrop) { 46 | this.backdrop = { 47 | element: this._backdrop.update(el), 48 | update: this._backdrop.update, 49 | } 50 | } 51 | } 52 | 53 | set backdrop(bd) { 54 | this._backdrop = bd 55 | 56 | const cur_child = this.$shadow.querySelector('visbug-boxmodel') 57 | 58 | cur_child 59 | ? this.$shadow.replaceChild(bd.element, cur_child) 60 | : this.$shadow.appendChild(bd.element) 61 | } 62 | 63 | render({ x, y, width, height, top, left }, node_label_id, isFixed) { 64 | this.$shadow.host.setAttribute('data-label-id', node_label_id) 65 | 66 | this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`) 67 | this.style.setProperty('--left', `${left}px`) 68 | this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute') 69 | this.style.setProperty('--width', `${width}px`) 70 | this.style.setProperty('--height', `${height}px`) 71 | 72 | return ` 73 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ` 90 | } 91 | } 92 | 93 | customElements.define('visbug-handles', Handles) 94 | -------------------------------------------------------------------------------- /app/components/selection/hover.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host rect { 4 | width: 100%; 5 | height: 100%; 6 | vector-effect: non-scaling-stroke; 7 | stroke: var(--hover-stroke, var(--neon-purple, hsl(267 100% 58%))); 8 | stroke-width: 2px; 9 | fill: none; 10 | } 11 | 12 | :host > svg { 13 | z-index: var(--layer-5); 14 | } 15 | -------------------------------------------------------------------------------- /app/components/selection/hover.element.js: -------------------------------------------------------------------------------- 1 | import { Handles } from './handles.element' 2 | import { HandlesStyles, HoverStyles } from '../styles.store' 3 | 4 | export class Hover extends Handles { 5 | 6 | constructor() { 7 | super() 8 | this.styles = [HandlesStyles, HoverStyles] 9 | } 10 | 11 | connectedCallback() { 12 | this.$shadow.adoptedStyleSheets = this.styles 13 | } 14 | 15 | disconnectedCallback() {} 16 | 17 | render({ width, height, top, left }, node_label_id, isFixed) { 18 | this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`) 19 | this.style.setProperty('--left', `${left}px`) 20 | this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute') 21 | 22 | return ` 23 | 27 | 28 | ` 29 | } 30 | } 31 | 32 | customElements.define('visbug-hover', Hover) 33 | -------------------------------------------------------------------------------- /app/components/selection/label.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host { 4 | font-size: 16px; 5 | position: initial; 6 | background: transparent; 7 | border: none; 8 | overflow: visible; 9 | padding: 0; 10 | margin: 0; 11 | --position: absolute; 12 | --top: 0; 13 | --left: 0; 14 | --max-width: 0; 15 | } 16 | 17 | :host > span { 18 | position: var(--position); 19 | inset: var(--top) auto auto var(--left); 20 | max-width: var(--max-width); 21 | z-index: var(--layer-4); 22 | transform: translateY(-100%); 23 | background: var(--label-bg, var(--neon-pink)); 24 | text-shadow: var(--text-shadow); 25 | color: white; 26 | display: inline-flex; 27 | justify-content: center; 28 | font-size: 0.8em; 29 | font-weight: normal; 30 | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; 31 | white-space: nowrap; 32 | padding: 2px 6px; 33 | line-height: 1.1; 34 | } 35 | 36 | :host a { 37 | text-decoration: none; 38 | color: inherit; 39 | cursor: pointer; 40 | text-overflow: ellipsis; 41 | overflow: hidden; 42 | padding-bottom: 1px; 43 | 44 | &:hover { 45 | text-decoration: underline; 46 | color: white; 47 | } 48 | 49 | &[node]:before { 50 | content: "\003c"; 51 | } 52 | 53 | &[node]:after { 54 | content: "\003e"; 55 | } 56 | } 57 | 58 | :host::backdrop { 59 | background: none !important; 60 | } 61 | -------------------------------------------------------------------------------- /app/components/selection/offscreenLabel.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | @import "./label.element.css"; 4 | 5 | :host { 6 | --count: "\00a0 0"; 7 | --hover-text: ""; 8 | } 9 | 10 | :host > [offscreen-label] { 11 | cursor: none; 12 | } 13 | 14 | :host > [offscreen-label]:after { 15 | content: var(--count); 16 | } 17 | 18 | :host > [offscreen-label]:hover:after { 19 | content: var(--hover-text); 20 | } -------------------------------------------------------------------------------- /app/components/selection/offscreenLabel.element.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { OffscreenLabelStyles } from '../styles.store' 3 | 4 | window.addEventListener('scroll', positionFlags) 5 | 6 | // hide offscreen label indicators if you click anywhere: 7 | document.body.addEventListener('click', () => { 8 | removeOffscreenLabelIndicators() 9 | }, true) 10 | 11 | function positionFlags() { 12 | removeOffscreenLabelIndicators() 13 | document.querySelectorAll('visbug-label').forEach((el) => { 14 | el.detectOutsideViewport() 15 | }) 16 | } 17 | 18 | export class OffscreenLabel extends HTMLElement { 19 | 20 | constructor() { 21 | super() 22 | this.$shadow = this.attachShadow({mode: 'closed'}) 23 | this.dispatchQuery = this.dispatchQuery.bind(this) 24 | } 25 | 26 | connectedCallback() { 27 | this.$shadow.adoptedStyleSheets = [OffscreenLabelStyles] 28 | $('a', this.$shadow).on('click mouseenter', this.dispatchQuery) 29 | } 30 | 31 | disconnectedCallback() { 32 | $('a', this.$shadow).off('click mouseenter', this.dispatchQuery) 33 | } 34 | 35 | dispatchQuery(e) { 36 | this.$shadow.host.dispatchEvent(new CustomEvent('query', { 37 | bubbles: true, 38 | detail: { 39 | text: e.target.textContent, 40 | activator: e.type, 41 | } 42 | })) 43 | } 44 | 45 | set text(content) { 46 | this.$shadow.childElementCount 47 | ? this.$shadow.firstElementChild.textContent = content 48 | : this._text = content 49 | } 50 | 51 | set position({boundingRect, node_label_id, isFixed}) { 52 | this.$shadow.innerHTML = this.render(node_label_id) 53 | this.update = {boundingRect, isFixed} 54 | } 55 | 56 | set update({boundingRect, isFixed}) { 57 | this.style.setProperty('--top', `${boundingRect.y + (isFixed ? 0 : window.scrollY)}px`) 58 | this.style.setProperty('--left', `${boundingRect.x - 1}px`) 59 | this.style.setProperty('--max-width', `${boundingRect.width + (window.innerWidth - boundingRect.x - boundingRect.width - 20)}px`) 60 | this.style.setProperty('--position', 'fixed'); 61 | } 62 | 63 | set count(count) { 64 | this.$shadow.childElementCount 65 | ? this.$shadow.firstElementChild.count = count 66 | : this._count = count 67 | } 68 | 69 | get count() { 70 | return this.$shadow.childElementCount 71 | ? this.$shadow.firstElementChild.count 72 | : this._count 73 | } 74 | 75 | render(node_label_id) { 76 | this.$shadow.host.setAttribute('data-label-id', node_label_id || ('label_' + Number(new Date()))) 77 | 78 | return `${this._text}` 79 | } 80 | } 81 | 82 | customElements.define('visbug-offscreen-label', OffscreenLabel) 83 | 84 | export function createOffscreenLabelIndicator(node_label_id, text, hoverText, left, top, color, adjustRightSideToCount) { 85 | const existing = document.querySelectorAll(`visbug-offscreen-label[id=${text}]`) 86 | 87 | if (existing.length) { 88 | const instance = existing[0]; 89 | instance.style.display = '' 90 | instance.style.setProperty('--left', left) 91 | instance.style.setProperty('--top', top) 92 | instance.style.setProperty('--position', 'fixed'); 93 | if (color) instance.style.setProperty('--label-bg', color) 94 | instance.seen[node_label_id] = true; 95 | instance.count = Object.keys(instance.seen).length 96 | instance.text = text 97 | instance.style.setProperty('--count', `"\\00a0 ${instance.count}"`); 98 | instance.style.setProperty('--hover-text', `"\\00a0 ${hoverText ? 'offscreen label: ' + hoverText : instance.count}"`); 99 | if (adjustRightSideToCount) { 100 | left = left.includes('calc(') ? left.replace(')', ` - ${instance.count.toString().length}ch)`) : `${instance.count.toString().length}ch` 101 | instance.style.setProperty('--left', left) 102 | } 103 | 104 | return 105 | } 106 | 107 | const label = document.createElement('visbug-offscreen-label') 108 | 109 | label.id = text 110 | label.position = { 111 | boundingRect: document.body.getBoundingClientRect(), 112 | isFixed: true, 113 | } 114 | label.seen = {} // reset 115 | label.seen[node_label_id] = true 116 | label.count = 1 117 | label.text = text 118 | label.style.display = '' 119 | label.style.setProperty('--left', left) 120 | label.style.setProperty('--top', top) 121 | label.style.setProperty('--count', `"\\00a0 ${label.count}"`) 122 | label.style.setProperty('--hover-text', `"\\00a0 ${hoverText ? 'offscreen label: ' + hoverText : label.count}"`) 123 | if (color) label.style.setProperty('--label-bg', color) 124 | 125 | if (adjustRightSideToCount) { 126 | left = left.includes('calc(') ? left.replace(')', ` - 1ch)`) : `1ch` 127 | label.style.setProperty('--left', left) 128 | } 129 | 130 | document.body.appendChild(label) 131 | } 132 | 133 | export function removeOffscreenLabelIndicators() { 134 | document.querySelectorAll('visbug-offscreen-label') 135 | .forEach(e => { 136 | e.seen = {} 137 | e.count = 0 138 | e.text = '' 139 | e.style.display = 'none' 140 | }) 141 | } -------------------------------------------------------------------------------- /app/components/selection/overlay.element.css: -------------------------------------------------------------------------------- 1 | @import "../_variables.css"; 2 | 3 | :host svg { 4 | display: none; 5 | position: absolute; 6 | top: var(--top); 7 | left: var(--left); 8 | overflow: visible; 9 | pointer-events: none; 10 | z-index: var(--layer-5); 11 | 12 | & > rect { 13 | fill: hsla(330, 100%, 71%, 0.5); 14 | width: 100%; 15 | height: 100%; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/selection/overlay.element.js: -------------------------------------------------------------------------------- 1 | import { OverlayStyles } from '../styles.store' 2 | 3 | export class Overlay extends HTMLElement { 4 | 5 | constructor() { 6 | super() 7 | this.$shadow = this.attachShadow({mode: 'closed'}) 8 | } 9 | 10 | connectedCallback() { 11 | this.$shadow.adoptedStyleSheets = [OverlayStyles] 12 | } 13 | 14 | disconnectedCallback() {} 15 | 16 | set position(boundingRect) { 17 | this.$shadow.innerHTML = this.render(boundingRect) 18 | } 19 | 20 | set update({ top, left, width, height }) { 21 | const [svg] = this.$shadow.children 22 | 23 | this.$shadow.host.style.display = 'block' 24 | svg.style.display = 'block' 25 | 26 | this.style.setProperty('--top', `${top + window.scrollY}px`) 27 | this.style.setProperty('--left', `${left - 1}px`) 28 | 29 | svg.setAttribute('width', width + 'px') 30 | svg.setAttribute('height', height + 'px') 31 | } 32 | 33 | render({height, width}) { 34 | return ` 35 | 39 | 40 | 41 | ` 42 | } 43 | } 44 | 45 | customElements.define('visbug-overlay', Overlay) 46 | -------------------------------------------------------------------------------- /app/components/styles.store.js: -------------------------------------------------------------------------------- 1 | import 'construct-style-sheets-polyfill' 2 | 3 | import { default as visbug_css } from './vis-bug/vis-bug.element.css' 4 | import { default as handles_css } from './selection/handles.element.css' 5 | import { default as handle_css } from './selection/handle.element.css' 6 | import { default as hover_css } from './selection/hover.element.css' 7 | import { default as corners_css } from './selection/corners.element.css' 8 | import { default as distance_css } from './selection/distance.element.css' 9 | import { default as gridline_css } from './selection/gridlines.element.css' 10 | import { default as label_css } from './selection/label.element.css' 11 | import { default as offscreenLabel_css } from './selection/offscreenLabel.element.css' 12 | import { default as overlay_css } from './selection/overlay.element.css' 13 | import { default as boxmodel_css } from './selection/box-model.element.css' 14 | import { default as metatip_css } from './metatip/metatip.element.css' 15 | import { default as hotkeymap_css } from './hotkey-map/base.element.css' 16 | import { default as grip_css } from './selection/grip.element.css' 17 | 18 | import { default as light_css } from './_variables_light.css' 19 | import { default as visbug_light_css } from './vis-bug/vis-bug.element_light.css' 20 | import { default as metatip_light_css } from './metatip/metatip.element_light.css' 21 | import { default as hotkeymap_light_css } from './hotkey-map/base.element_light.css' 22 | 23 | import { default as dark_css } from './_variables_dark.css' 24 | import { default as visbug_dark_css } from './vis-bug/vis-bug.element_dark.css' 25 | import { default as metatip_dark_css } from './metatip/metatip.element_dark.css' 26 | import { default as hotkeymap_dark_css } from "./hotkey-map/base.element_dark.css" 27 | 28 | const constructStylesheet = (styles, stylesheet = new CSSStyleSheet()) => { 29 | stylesheet.replaceSync(styles) 30 | return stylesheet 31 | } 32 | 33 | export const VisBugStyles = constructStylesheet(visbug_css) 34 | export const HandlesStyles = constructStylesheet(handles_css) 35 | export const HandleStyles = constructStylesheet(handle_css) 36 | export const HoverStyles = constructStylesheet(hover_css) 37 | export const CornerStyles = constructStylesheet(corners_css) 38 | export const MetatipStyles = constructStylesheet(metatip_css) 39 | export const DistanceStyles = constructStylesheet(distance_css) 40 | export const GridlineStyles = constructStylesheet(gridline_css) 41 | export const LabelStyles = constructStylesheet(label_css) 42 | export const OffscreenLabelStyles = constructStylesheet(offscreenLabel_css) 43 | export const OverlayStyles = constructStylesheet(overlay_css) 44 | export const BoxModelStyles = constructStylesheet(boxmodel_css) 45 | export const HotkeymapStyles = constructStylesheet(hotkeymap_css) 46 | export const GripStyles = constructStylesheet(grip_css) 47 | 48 | export const LightTheme = constructStylesheet(light_css) 49 | export const VisBugLightStyles = constructStylesheet(visbug_light_css) 50 | export const MetatipLightStyles = constructStylesheet(metatip_light_css) 51 | export const HotkeymapLightStyles = constructStylesheet(hotkeymap_light_css) 52 | 53 | export const DarkTheme = constructStylesheet(dark_css) 54 | export const VisBugDarkStyles = constructStylesheet(visbug_dark_css) 55 | export const MetatipDarkStyles = constructStylesheet(metatip_dark_css) 56 | export const HotkeymapDarkStyles = constructStylesheet(hotkeymap_dark_css) 57 | -------------------------------------------------------------------------------- /app/components/vis-bug/vis-bug.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 100vh; 3 | } 4 | 5 | body:not([testing]) [data-selected=true] { 6 | transition: all 0.15s ease; 7 | } 8 | 9 | [data-pseudo-select=true] { 10 | outline: 1px dashed hsl(267, 100%, 58%); 11 | } 12 | 13 | [data-selected-hide=true][data-selected=true]:after { 14 | display: none; 15 | } 16 | 17 | [data-selected=true][contenteditable=true] { 18 | caret-color: var(--neon-pink); 19 | } 20 | 21 | [data-measuring=true] { 22 | cursor: crosshair; 23 | } 24 | 25 | [draggable=true] { 26 | cursor: grab; 27 | 28 | &:active { 29 | cursor: grabbing; 30 | } 31 | 32 | & :is(a, img) { 33 | -webkit-user-drag: none !important; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/components/vis-bug/vis-bug.element_dark.css: -------------------------------------------------------------------------------- 1 | :host > ol { 2 | &:first-of-type { 3 | box-shadow: 0 0.25em 0.5em hsla(0,0%,0%,50%); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/components/vis-bug/vis-bug.element_light.css: -------------------------------------------------------------------------------- 1 | :host > ol { 2 | &:first-of-type { 3 | box-shadow: 0 0.25em 0.5em hsla(0,0%,0%,10%); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/components/vis-bug/vis-bug.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, getActiveTool, pptrMetaKey } 4 | from '../../../tests/helpers' 5 | 6 | test.beforeEach(setupPptrTab) 7 | 8 | test('Should have guides as default tool', async t => { 9 | const { page } = t.context 10 | t.is(await getActiveTool(page), 'guides') 11 | t.pass() 12 | }) 13 | 14 | test('Should have 13 tools', async t => { 15 | const { page } = t.context 16 | const tools = await page.evaluate(`document.querySelector('vis-bug').$shadow.querySelectorAll('ol:first-of-type > li').length`) 17 | 18 | t.is(tools, 13) 19 | t.pass() 20 | }) 21 | 22 | test('Should have 13 key trainers', async t => { 23 | const { page } = t.context 24 | const trainers = await page.evaluate(`document.querySelector('vis-bug').$shadow.querySelectorAll('visbug-hotkeys > *').length`) 25 | 26 | t.is(trainers, 13) 27 | t.pass() 28 | }) 29 | 30 | test('Should have 3 color pickers', async t => { 31 | const { page } = t.context 32 | const pickers = await page.evaluate(`document.querySelector('vis-bug').$shadow.querySelectorAll('ol[colors] > li').length`) 33 | 34 | t.is(pickers, 3) 35 | t.pass() 36 | }) 37 | 38 | test('Should allow selecting 1 element', async t => { 39 | const { page } = t.context 40 | 41 | await page.click(`[intro]`) 42 | 43 | const handles_elements = await page.evaluate(`document.querySelectorAll('visbug-handles').length`) 44 | 45 | t.is(handles_elements, 1) 46 | 47 | t.pass() 48 | }) 49 | 50 | test('Should allow multi-selection', async t => { 51 | const { page } = t.context 52 | 53 | await page.click(`.artboard:nth-of-type(1)`) 54 | await page.keyboard.down('Shift') 55 | await page.click(`.artboard:nth-of-type(2)`) 56 | await page.keyboard.up('Shift') 57 | 58 | const handles_elements = await page.evaluate(`document.querySelectorAll('visbug-handles').length`) 59 | 60 | t.is(handles_elements, 2) 61 | 62 | t.pass() 63 | }) 64 | 65 | test('Should allow deselecting', async t => { 66 | const { page } = t.context 67 | 68 | await page.click(`.artboard:nth-of-type(1)`) 69 | const handles_elements = await page.evaluate(`document.querySelectorAll('visbug-handles').length`) 70 | t.is(handles_elements, 1) 71 | 72 | await page.keyboard.press('Escape') 73 | const new_handles_elements = await page.evaluate(`document.querySelectorAll('visbug-handles').length`) 74 | t.is(new_handles_elements, 0) 75 | 76 | t.pass() 77 | }) 78 | 79 | test('Should be hideable', async t => { 80 | const { page } = t.context 81 | const metaKey = await pptrMetaKey(page) 82 | 83 | await page.keyboard.down(metaKey) 84 | await page.keyboard.down('.') 85 | await page.keyboard.up(metaKey) 86 | await page.keyboard.up('.') 87 | 88 | const visibility = await page.evaluate(`document.querySelector('vis-bug').$shadow.host.style.display`) 89 | 90 | t.is(visibility, 'none') 91 | t.pass() 92 | }) 93 | 94 | test('Should accept valid execCommand', async t => { 95 | const { page } = t.context 96 | const execCommand = await page.evaluate(`document.querySelector('vis-bug').execCommand('shuffle')`) 97 | 98 | t.is(execCommand, undefined) 99 | t.pass() 100 | }) 101 | 102 | test('Should throw on invalid execCommand', async t => { 103 | const { page } = t.context 104 | const execCommand = await page.evaluate(`document.querySelector('vis-bug').execCommand('invalid command')`) 105 | 106 | t.deepEqual(execCommand, {}) 107 | t.pass() 108 | }) 109 | 110 | test.afterEach(teardownPptrTab) 111 | -------------------------------------------------------------------------------- /app/demo/artboard.css: -------------------------------------------------------------------------------- 1 | .artboard { 2 | background: white; 3 | color: var(--light-grey); 4 | box-shadow: 0 1px 2px 1px hsla(0,0%,0%,5%); 5 | position: relative; 6 | z-index: 1; 7 | margin-top: 1.5rem; 8 | 9 | &[flex] { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | 15 | &[padded], & > [padded] { 16 | padding: 0.5rem; 17 | } 18 | 19 | &.dark { 20 | background: var(--dark-grey); 21 | border: 1px solid var(--darker-grey); 22 | box-shadow: none; 23 | color: var(--lighter-grey); 24 | } 25 | 26 | &.message { 27 | display: flex; 28 | justify-content: center; 29 | flex-direction: column; 30 | padding: 1.5rem; 31 | 32 | & > h2 { 33 | font-weight: bold; 34 | } 35 | 36 | & > p { 37 | line-height: 1.5; 38 | margin-bottom: 0; 39 | } 40 | } 41 | 42 | & > label { 43 | content: attr(artboard); 44 | position: absolute; 45 | top: -25px; 46 | left: 0; 47 | right: 0; 48 | height: 20px; 49 | display: flex; 50 | align-items: center; 51 | font-size: 0.8rem; 52 | font-weight: lighter; 53 | color: var(--lightest-grey); 54 | white-space: nowrap; 55 | 56 | & > summary { 57 | display: inline-flex; 58 | align-items: center; 59 | } 60 | 61 | & > summary > svg { 62 | height: 20px; 63 | margin-right: 0.5rem; 64 | } 65 | 66 | & > b { 67 | color: var(--theme-color); 68 | } 69 | 70 | & > :matches(b, .material-icons) { 71 | margin: 0 0.25em; 72 | } 73 | 74 | &:hover { 75 | color: var(--lightest-grey); 76 | } 77 | 78 | &[with-info] { 79 | justify-content: space-between; 80 | 81 | & i { 82 | cursor: pointer; 83 | font-style: normal; 84 | border: 1px solid var(--dark-grey); 85 | border-radius: 50%; 86 | width: 1.4em; 87 | height: 1.4em; 88 | display: inline-flex; 89 | align-items: center; 90 | justify-content: center; 91 | line-height: 1.1; 92 | 93 | &:hover { 94 | color: var(--theme-color); 95 | border-color: var(--theme-color); 96 | } 97 | } 98 | } 99 | } 100 | 101 | & .tips { 102 | display: grid; 103 | grid-gap: 0.5rem; 104 | border: 1px solid var(--darkest-grey); 105 | border-radius: 1rem; 106 | padding: 0.5rem; 107 | 108 | & > p { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | font-size: 0.8rem; 112 | 113 | @media (max-width: 480px) { 114 | font-size: 1rem; 115 | } 116 | } 117 | } 118 | 119 | & > [grid][vertically-aligned] { 120 | padding: 0.5rem; 121 | box-sizing: border-box; 122 | width: 100%; 123 | height: auto; 124 | 125 | & > h2 { 126 | margin: 0; 127 | } 128 | } 129 | 130 | & [fit-height] { 131 | flex: 1; 132 | } 133 | 134 | &.pictures img{ 135 | object-fit: cover; 136 | width: 100%; 137 | height: 100%; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/demo/card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background: #fff; 3 | max-width: 400px; 4 | box-shadow: 0 0.2em 0.3em hsla(0,0%,0%,20%); 5 | padding: 1rem; 6 | border-radius: 0.2em; 7 | border: 1px solid hsl(0,0%,90%); 8 | 9 | & > .card-header { 10 | & h2, & small { 11 | margin: 0; 12 | } 13 | 14 | & > img { 15 | width: 4rem; 16 | height: 4rem; 17 | border-radius: 50%; 18 | overflow: hidden; 19 | object-fit: cover; 20 | } 21 | 22 | & > div { 23 | margin-left: 1rem; 24 | } 25 | } 26 | 27 | & > .card-content { 28 | margin-top: 0.5rem; 29 | margin-bottom: 0.5rem; 30 | 31 | & img { 32 | width: 100%; 33 | height: 200px; 34 | object-fit: cover; 35 | } 36 | } 37 | 38 | & > .card-footer { 39 | padding-top: 0.5rem; 40 | border-top: 1px solid #e6e6e6; 41 | 42 | & > button { 43 | padding: 0.5rem 1rem; 44 | } 45 | 46 | & time { 47 | flex: 2; 48 | text-align: right; 49 | } 50 | } 51 | 52 | & date, & time { 53 | font-size: 0.8rem; 54 | color: #808080; 55 | } 56 | } 57 | 58 | .mdc-card { 59 | max-width: 400px; 60 | } -------------------------------------------------------------------------------- /app/demo/layout.css: -------------------------------------------------------------------------------- 1 | main { 2 | --card-size: 70vw; 3 | display: grid; 4 | grid-template-columns: repeat(auto-fit, minmax(var(--card-size), 1fr)); 5 | grid-auto-rows: minmax(var(--card-size), max-content); 6 | grid-auto-flow: dense; 7 | gap: 1rem 2rem; 8 | padding: 1rem 2rem; 9 | 10 | @media (min-width:480px) { 11 | --card-size: 35vw; 12 | } 13 | 14 | @media (min-width:768px) { 15 | --card-size: 20vw; 16 | 17 | & > article { 18 | &[span2x2] { 19 | grid-row: span 2; 20 | grid-column: span 2; 21 | } 22 | 23 | &[span3x1] { 24 | grid-column: span 3; 25 | } 26 | 27 | &[span2x1] { 28 | grid-column: span 2; 29 | } 30 | 31 | &[span1x2] { 32 | grid-row: span 2; 33 | } 34 | 35 | &[span1x1] { 36 | grid-row: span 1; 37 | grid-column: span 1; 38 | } 39 | } 40 | } 41 | 42 | @media (min-width:1024px) { 43 | --card-size: 16vw; 44 | } 45 | 46 | @media (min-width:1600px) { 47 | --card-size: 15vw; 48 | } 49 | } 50 | 51 | [image-grid] { 52 | display: grid; 53 | gap: .5rem; 54 | height: 100%; 55 | align-items: space-between; 56 | justify-content: space-between; 57 | grid-template-columns: 1fr 1fr; 58 | grid-template-rows: auto 1fr 1fr; 59 | 60 | & > div:first-child { 61 | grid-column: span 2; 62 | } 63 | 64 | & img { 65 | height: 100%; 66 | width: 100%; 67 | object-fit: contain; 68 | object-position: center center; 69 | } 70 | } 71 | 72 | [text-styles] { 73 | margin-top: 1rem; 74 | display: grid; 75 | grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr; 76 | gap: .5rem; 77 | 78 | & :matches(h1,h2,h3,h4,h5,h6) { 79 | margin: 0; 80 | } 81 | } 82 | 83 | [color-grid] { 84 | display: grid; 85 | grid-template-columns: 1fr 1fr; 86 | grid-template-rows: 1fr 1fr; 87 | gap: .5rem; 88 | 89 | & > div { 90 | border-radius: 1rem; 91 | display: inline-flex; 92 | align-items: center; 93 | justify-content: center; 94 | text-align: center; 95 | 96 | &:not(:first-of-type) { 97 | color: white; 98 | } 99 | } 100 | } 101 | 102 | [stop-light] { 103 | display: grid; 104 | align-items: center; 105 | justify-content: space-around; 106 | 107 | & > .filled-circle { 108 | width: 7rem; 109 | height: 7rem; 110 | } 111 | } 112 | 113 | [ordered-numbers] { 114 | display: grid; 115 | width: 100%; 116 | grid-template-columns: repeat(5, 5rem); 117 | justify-content: space-around; 118 | 119 | & > .filled-circle:nth-child(1) { background: hsl(200, 50%, 50%); } 120 | & > .filled-circle:nth-child(2) { background: hsl(225, 50%, 50%); } 121 | & > .filled-circle:nth-child(3) { background: hsl(250, 50%, 50%); } 122 | & > .filled-circle:nth-child(4) { background: hsl(275, 50%, 50%); } 123 | & > .filled-circle:nth-child(5) { background: hsl(300, 50%, 50%); } 124 | } 125 | 126 | nav { 127 | display: grid; 128 | grid-auto-columns: 1fr; 129 | grid-auto-flow: column; 130 | 131 | &[margin] { 132 | grid-auto-flow: row; 133 | } 134 | 135 | & > a { 136 | text-align: center; 137 | margin: 5px; 138 | border: 1px solid var(--darkest-grey); 139 | border-radius: .25rem; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/demo/mock-ad.css: -------------------------------------------------------------------------------- 1 | [mock-ad] { 2 | & h2 { 3 | font-weight: bold; 4 | font-size: 4rem; 5 | color: red; 6 | border: 2px solid red; 7 | padding: 2rem; 8 | animation: pulse-annoyingly 1s linear infinite; 9 | } 10 | } 11 | 12 | @keyframes pulse-annoyingly { 13 | 0%, 100% { 14 | transform: scale(1); 15 | } 16 | 50% { 17 | transform: scale(0.9); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/demo/multi-select.css: -------------------------------------------------------------------------------- 1 | [multi-select] { 2 | flex: 2; 3 | display: grid; 4 | grid-template-columns: repeat(auto-fit, 2.5rem); 5 | grid-auto-rows: 2.5rem; 6 | grid-auto-flow: dense; 7 | justify-content: space-between; 8 | grid-gap: 1rem; 9 | padding: 0 1rem; 10 | height: 100%; 11 | box-sizing: border-box; 12 | 13 | & > span { 14 | background: var(--grey); 15 | border-radius: 50%; 16 | 17 | &:hover { 18 | background: var(--theme-color); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/demo/shapes.css: -------------------------------------------------------------------------------- 1 | .outlined-square { 2 | border: 1px solid var(--light-grey); 3 | width: 10rem; 4 | height: 10rem; 5 | } 6 | 7 | .filled-square { 8 | background: var(--light-grey); 9 | width: 4rem; 10 | height: 4rem; 11 | } 12 | 13 | .filled-circle { 14 | background: var(--grey); 15 | border-radius: 50%; 16 | width: 5rem; 17 | height: 5rem; 18 | display: inline-flex; 19 | align-items: center; 20 | justify-content: center; 21 | color: white; 22 | 23 | &.small { width: 2.5rem; height: 2.5rem; } 24 | } 25 | 26 | 27 | .red { background-color: red; } 28 | .green { background-color: green; } 29 | .yellow { background-color: yellow; } 30 | .google-blue { background-color: #4285F4; } 31 | .google-yellow { background-color: #F4B400; } 32 | .google-red { background-color: #DB4437; } 33 | .google-green { background-color: #0F9D58; } 34 | .shadow-circle { background-color: white; box-shadow: 0 10px 30px 2px hsla(0,0%,0%,20%); } 35 | 36 | [round-icon] { 37 | box-sizing: border-box; 38 | padding: 1rem; 39 | width: var(--card-size); 40 | height: var(--card-size); 41 | border: 5px solid var(--light-grey); 42 | border-radius: 50%; 43 | fill: var(--grey); 44 | } 45 | 46 | [bgfg] { 47 | width: 100%; 48 | display: grid; 49 | grid-gap: 0.5rem; 50 | grid-template-columns: 15vw 15vw; 51 | grid-template-rows: 15vw 15vw; 52 | justify-content: space-around; 53 | margin-top: 0.5rem; 54 | 55 | & > .filled-circle { 56 | font-size: 5vw; 57 | width: 100%; 58 | height: 100%; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/demo/typography.css: -------------------------------------------------------------------------------- 1 | h1,h2,h3 { 2 | font-family: "Google Sans", system-ui; 3 | font-weight: lighter; 4 | line-height: 1.2; 5 | margin: 0; 6 | } 7 | 8 | small { 9 | color: var(--mid-grey); 10 | } 11 | 12 | a { 13 | color: var(--theme-color); 14 | 15 | &[href]:not(:hover) { 16 | text-decoration: none; 17 | } 18 | } 19 | 20 | [text-styles] { 21 | padding: 0 2rem 2rem; 22 | } 23 | 24 | [bad-contrast] { 25 | padding: 1rem 2rem; 26 | background: hsl(0,0%,40%); 27 | color: hsl(0,0%,70%); 28 | } 29 | 30 | [good-contrast] { 31 | padding: 1rem 2rem; 32 | font-weight: bold; 33 | background: black; 34 | color: white; 35 | } 36 | -------------------------------------------------------------------------------- /app/extension.css: -------------------------------------------------------------------------------- 1 | @import 'components/vis-bug/vis-bug.css'; 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/favicon.ico -------------------------------------------------------------------------------- /app/features/accessibility.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { setupPptrTab, teardownPptrTab, getActiveTool, changeMode } from '../../tests/helpers' 3 | 4 | test.beforeEach(setupPptrTab) 5 | 6 | const contrastValueSelector = `document.querySelector('visbug-ally').$shadow.querySelector('span[contrast]').textContent.trim()` 7 | 8 | test('Can be activated', async t => { 9 | const {page} = t.context; 10 | await changeMode({page, tool: 'accessibility'}) 11 | 12 | t.is(await getActiveTool(page), 'accessibility') 13 | t.pass() 14 | }) 15 | 16 | // test('Can reveal color contrasts between html nodes and backgrounds', async t => { 17 | // const {page} = t.context; 18 | // await changeMode({page, tool: 'accessibility'}) 19 | 20 | // await page.hover('.google-blue') 21 | // const blueContrastValue = await page.evaluate(contrastValueSelector) 22 | // t.is(blueContrastValue, "3.56") 23 | // t.pass() 24 | 25 | 26 | // await page.hover('.google-red') 27 | // const redContrastValue = await page.evaluate(contrastValueSelector) 28 | // t.is(redContrastValue, "4.29") 29 | // t.pass() 30 | 31 | // await page.hover('.google-yellow') 32 | // const yellowContrastValue = await page.evaluate(contrastValueSelector) 33 | // t.is(yellowContrastValue, "1.84") 34 | // t.pass() 35 | // }) 36 | 37 | test('Does not show a11y tooltip on node', async t => { 38 | const {page} = t.context; 39 | await changeMode({page, tool: 'accessibility'}) 40 | 41 | const svgEl = await page.$('svg') 42 | const {x, y} = await svgEl.boundingBox() 43 | await page.mouse.click(x + 1, y + 1) // an empty space of the first svg element 44 | const targetNodeName = await page.$eval('[data-selected="true"]', el => el.nodeName) 45 | t.is(targetNodeName, 'svg') 46 | t.pass() 47 | 48 | t.is(await page.$('visbug-ally'), null) 49 | t.pass() 50 | }) 51 | 52 | test('Gets fill or stroke value first if the target is one of svg elements', async t => { 53 | const {page} = t.context; 54 | await changeMode({page, tool: 'accessibility'}) 55 | 56 | await page.hover('svg') 57 | const pathContrastValue = await page.evaluate(contrastValueSelector) 58 | t.not(pathContrastValue, "10.44") 59 | t.pass() 60 | }) 61 | 62 | test.afterEach(teardownPptrTab) 63 | -------------------------------------------------------------------------------- /app/features/boxshadow.js: -------------------------------------------------------------------------------- 1 | import hotkeys from 'hotkeys-js' 2 | import { metaKey, getStyle, showHideSelected } from '../utilities/' 3 | 4 | const key_events = 'up,down,left,right' 5 | .split(',') 6 | .reduce((events, event) => 7 | `${events},${event},shift+${event},alt+${event},alt+shift+${event}` 8 | , '') 9 | .substring(1) 10 | 11 | const command_events = `${metaKey}+up,${metaKey}+shift+up,${metaKey}+down,${metaKey}+shift+down,${metaKey}+left,${metaKey}+shift+left,${metaKey}+right,${metaKey}+shift+right` 12 | 13 | export function BoxShadow({selection}) { 14 | hotkeys(key_events, (e, handler) => { 15 | if (e.cancelBubble) return 16 | 17 | e.preventDefault() 18 | 19 | let selectedNodes = selection() 20 | , keys = handler.key.split('+') 21 | 22 | if (keys.includes('left') || keys.includes('right')) 23 | keys.includes('alt') 24 | ? changeBoxShadow(selectedNodes, keys, 'size') 25 | : changeBoxShadow(selectedNodes, keys, 'x') 26 | else 27 | keys.includes('alt') 28 | ? changeBoxShadow(selectedNodes, keys, 'blur') 29 | : changeBoxShadow(selectedNodes, keys, 'y') 30 | }) 31 | 32 | hotkeys(command_events, (e, handler) => { 33 | e.preventDefault() 34 | let keys = handler.key.split('+') 35 | keys.includes('left') || keys.includes('right') 36 | ? changeBoxShadow(selection(), keys, 'opacity') 37 | : changeBoxShadow(selection(), keys, 'inset') 38 | }) 39 | 40 | return () => { 41 | hotkeys.unbind(key_events) 42 | hotkeys.unbind(command_events) 43 | hotkeys.unbind('up,down,left,right') 44 | } 45 | } 46 | 47 | const ensureHasShadow = el => { 48 | if (el.style.boxShadow == '' || el.style.boxShadow == 'none') 49 | el.style.boxShadow = 'hsla(0,0%,0%,30%) 0 0 0 0' 50 | return el 51 | } 52 | 53 | // todo: work around this propMap with a better split 54 | const propMap = { 55 | 'opacity': 3, 56 | 'x': 4, 57 | 'y': 5, 58 | 'blur': 6, 59 | 'size': 7, 60 | 'inset': 8, 61 | } 62 | 63 | const parseCurrentShadow = el => getStyle(el, 'boxShadow').split(' ') 64 | 65 | export function changeBoxShadow(els, direction, prop) { 66 | els 67 | .map(ensureHasShadow) 68 | .map(el => showHideSelected(el, 1500)) 69 | .map(el => ({ 70 | el, 71 | style: 'boxShadow', 72 | current: parseCurrentShadow(el), // ["rgb(255,", "0,", "0)", "0px", "0px", "1px", "0px"] 73 | propIndex: parseCurrentShadow(el)[0].includes('rgba') ? propMap[prop] : propMap[prop] - 1 74 | })) 75 | .map(payload => { 76 | let updated = [...payload.current] 77 | let cur = prop === 'opacity' 78 | ? payload.current[payload.propIndex] 79 | : parseInt(payload.current[payload.propIndex]) 80 | 81 | switch(prop) { 82 | case 'blur': 83 | case 'size': 84 | var amount = direction.includes('shift') ? 10 : 1 85 | updated[payload.propIndex] = direction.includes('down') || direction.includes('left') 86 | ? `${cur - amount}px` 87 | : `${cur + amount}px` 88 | break 89 | case 'inset': 90 | updated[payload.propIndex] = direction.includes('down') 91 | ? 'inset' 92 | : '' 93 | break 94 | case 'opacity': 95 | let cur_opacity = parseFloat(cur.slice(0, cur.indexOf(')'))) 96 | var amount = direction.includes('shift') ? 0.10 : 0.01 97 | updated[payload.propIndex] = direction.includes('left') 98 | ? cur_opacity - amount + ')' 99 | : cur_opacity + amount + ')' 100 | break 101 | default: 102 | var amount = direction.includes('shift') ? 10 : 1 103 | updated[payload.propIndex] = direction.includes('left') || direction.includes('up') 104 | ? `${cur - amount}px` 105 | : `${cur + amount}px` 106 | break 107 | } 108 | 109 | payload.value = updated 110 | return payload 111 | }) 112 | .forEach(({el, style, value}) => 113 | el.style[style] = value.join(' ')) 114 | } 115 | -------------------------------------------------------------------------------- /app/features/boxshadow.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool, pptrMetaKey } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'boxshadow' 7 | const test_selector = '[intro] b' 8 | 9 | const getShadowValues = async (page, testEl = test_selector) => { 10 | const shadowStr = await page.$eval(testEl, el => el.style.boxShadow) 11 | return parseShadowValues(shadowStr) 12 | } 13 | 14 | const parseShadowValues = (str) => { 15 | const [,color,x,y,blur,spread,inset] = /([^\)]+\)) ([^\s]+) ([^\s]+) ([^\s]+) ([^\s]+)( inset)?/.exec(str) 16 | return { color, x, y, blur, spread, inset : inset !== undefined } 17 | } 18 | 19 | test.beforeEach(async t => { 20 | await setupPptrTab(t) 21 | 22 | await changeMode({ 23 | tool, 24 | page: t.context.page, 25 | }) 26 | }) 27 | 28 | test('Can Be Activated', async t => { 29 | const { page } = t.context 30 | t.is(await getActiveTool(page), tool) 31 | t.pass() 32 | }) 33 | 34 | test('Can adjust X position', async t => { 35 | const { page } = t.context 36 | 37 | await page.click(test_selector) 38 | await page.keyboard.press('ArrowRight') 39 | let shadow = await getShadowValues(page) 40 | t.true(shadow.x === "1px") 41 | //test shift case 42 | await page.keyboard.down('Shift') 43 | await page.keyboard.press('ArrowRight') 44 | shadow = await getShadowValues(page) 45 | t.true(shadow.x === "11px") 46 | 47 | t.pass() 48 | }) 49 | 50 | 51 | test('Can adjust Y position', async t => { 52 | const { page } = t.context 53 | 54 | await page.click(test_selector) 55 | await page.keyboard.press('ArrowDown') 56 | let shadow = await getShadowValues(page) 57 | t.true(shadow.y === "1px") 58 | //test shift case 59 | await page.keyboard.down('Shift') 60 | await page.keyboard.press('ArrowDown') 61 | shadow = await getShadowValues(page) 62 | t.true(shadow.y === "11px") 63 | 64 | t.pass() 65 | }) 66 | 67 | test('Shadow Blur Works', async t => { 68 | const { page } = t.context 69 | 70 | await page.click(test_selector) 71 | await page.keyboard.press('ArrowDown') 72 | await page.keyboard.down('Alt') 73 | await page.keyboard.press('ArrowUp') 74 | let shadow = await getShadowValues(page) 75 | t.true(shadow.blur === "1px") 76 | //test shift case 77 | await page.keyboard.down('Shift') 78 | await page.keyboard.press('ArrowUp') 79 | shadow = await getShadowValues(page) 80 | t.true(shadow.blur === "11px") 81 | 82 | t.pass() 83 | }) 84 | 85 | test('Shadow Spread Works', async t => { 86 | const { page } = t.context 87 | 88 | await page.click(test_selector) 89 | await page.keyboard.press('ArrowDown') 90 | await page.keyboard.down('Alt') 91 | await page.keyboard.press('ArrowRight') 92 | let shadow = await getShadowValues(page) 93 | t.true(shadow.spread === "1px") 94 | //test shift case 95 | await page.keyboard.down('Shift') 96 | await page.keyboard.press('ArrowRight') 97 | shadow = await getShadowValues(page) 98 | t.true(shadow.spread === "11px") 99 | 100 | t.pass() 101 | }) 102 | 103 | test('Shadow can be set to inset', async t => { 104 | const { page } = t.context 105 | 106 | await page.click(test_selector) 107 | await page.keyboard.press('ArrowDown') 108 | await page.keyboard.down(await pptrMetaKey(page)) 109 | await page.keyboard.press('ArrowDown') 110 | const shadow = await getShadowValues(page) 111 | t.true(shadow.inset) 112 | 113 | t.pass() 114 | }) 115 | 116 | 117 | test.afterEach(teardownPptrTab) 118 | -------------------------------------------------------------------------------- /app/features/font.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool, pptrMetaKey } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'font' 7 | const test_selector = '[intro] b' 8 | 9 | const getInlineStyle = async (page, prop) => 10 | await page.$eval(test_selector, (el, prop) => { 11 | return el.style[prop] 12 | }, prop) 13 | 14 | test.beforeEach(async t => { 15 | await setupPptrTab(t) 16 | 17 | await changeMode({ 18 | tool, 19 | page: t.context.page, 20 | }) 21 | }) 22 | 23 | test('Can Be Activated', async t => { 24 | const { page } = t.context 25 | t.is(await getActiveTool(page), tool) 26 | t.pass() 27 | }) 28 | 29 | test('Can Be Deactivated', async t => { 30 | const { page } = t.context 31 | 32 | t.is(await getActiveTool(page), tool) 33 | await changeMode({ tool: 'padding', page }) 34 | t.is(await getActiveTool(page), 'padding') 35 | 36 | t.pass() 37 | }) 38 | 39 | test('Can change size', async t => { 40 | const { page } = t.context 41 | 42 | await page.click(test_selector) 43 | t.is(await getInlineStyle(page, 'font-size'), '') 44 | 45 | await page.keyboard.press('ArrowUp') 46 | t.is(await getInlineStyle(page, 'font-size'), '17px') 47 | 48 | await page.keyboard.press('ArrowDown') 49 | await page.keyboard.press('ArrowDown') 50 | t.is(await getInlineStyle(page, 'font-size'), '15px') 51 | 52 | t.pass() 53 | }) 54 | 55 | test('Can change alignment', async t => { 56 | const { page } = t.context 57 | 58 | await page.click(test_selector) 59 | t.is(await getInlineStyle(page, 'text-align'), '') 60 | 61 | await page.keyboard.press('ArrowRight') 62 | t.is(await getInlineStyle(page, 'text-align'), 'right') 63 | 64 | await page.keyboard.press('ArrowLeft') 65 | t.is(await getInlineStyle(page, 'text-align'), 'center') 66 | 67 | await page.keyboard.press('ArrowLeft') 68 | t.is(await getInlineStyle(page, 'text-align'), 'left') 69 | 70 | t.pass() 71 | }) 72 | 73 | test('Can change leading', async t => { 74 | const { page } = t.context 75 | 76 | await page.click(test_selector) 77 | t.is(await getInlineStyle(page, 'line-height'), '') 78 | 79 | await page.keyboard.down('Shift') 80 | await page.keyboard.press('ArrowUp') 81 | await page.keyboard.up('Shift') 82 | t.is(await getInlineStyle(page, 'line-height'), '20px') 83 | 84 | await page.keyboard.down('Shift') 85 | await page.keyboard.press('ArrowDown') 86 | await page.keyboard.up('Shift') 87 | t.is(await getInlineStyle(page, 'line-height'), '19px') 88 | 89 | t.pass() 90 | }) 91 | 92 | test('Can change letter space', async t => { 93 | const { page } = t.context 94 | 95 | await page.click(test_selector) 96 | t.is(await getInlineStyle(page, 'letter-spacing'), '') 97 | 98 | await page.keyboard.down('Shift') 99 | await page.keyboard.press('ArrowRight') 100 | await page.keyboard.up('Shift') 101 | t.is(await getInlineStyle(page, 'letter-spacing'), '1.6px') 102 | 103 | await page.keyboard.down('Shift') 104 | await page.keyboard.press('ArrowLeft') 105 | await page.keyboard.up('Shift') 106 | t.is(await getInlineStyle(page, 'letter-spacing'), '1.5px') 107 | 108 | t.pass() 109 | }) 110 | 111 | test('Can change weight', async t => { 112 | const { page } = t.context 113 | const metaKey = await pptrMetaKey(page) 114 | 115 | await page.click(test_selector) 116 | t.is(await getInlineStyle(page, 'font-weight'), '') 117 | 118 | await page.keyboard.down(metaKey) 119 | await page.keyboard.press('ArrowUp') 120 | await page.keyboard.up(metaKey) 121 | t.is(await getInlineStyle(page, 'font-weight'), '800') 122 | 123 | await page.keyboard.down(metaKey) 124 | await page.keyboard.press('ArrowDown') 125 | await page.keyboard.up(metaKey) 126 | t.is(await getInlineStyle(page, 'font-weight'), '700') 127 | 128 | t.pass() 129 | }) 130 | 131 | test.afterEach(teardownPptrTab) 132 | -------------------------------------------------------------------------------- /app/features/guides.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { isOffBounds, deepElementFromPoint } from '../utilities/' 3 | import { clearMeasurements, takeMeasurementOwnership } from './measurements' 4 | 5 | const state = { 6 | gridlines: null, 7 | measurements: null, 8 | stuck: { 9 | count: 0, 10 | measurements: [], 11 | }, 12 | } 13 | 14 | export function Guides(visbug) { 15 | $('body').on('mousemove', on_hover) 16 | $('body').on('mouseout', on_hoverout) 17 | 18 | window.addEventListener('scroll', hideGridlines) 19 | visbug.onSelectedUpdate(stickGuide) 20 | 21 | return () => { 22 | $('body').off('mousemove', on_hover) 23 | $('body').off('mouseout', on_hoverout) 24 | 25 | window.removeEventListener('scroll', hideGridlines) 26 | visbug.removeSelectedCallback(stickGuide) 27 | 28 | clearMeasurements() 29 | hideGridlines() 30 | } 31 | } 32 | 33 | const on_hover = e => { 34 | const target = deepElementFromPoint(e.clientX, e.clientY) 35 | if (isOffBounds(target)) return 36 | showGridlines(target) 37 | } 38 | 39 | export function createGuide(vert = true) { 40 | let guide = document.createElement('div') 41 | let styles = ` 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | background: hsla(330, 100%, 71%, 70%); 46 | pointer-events: none; 47 | z-index: 2147483643; 48 | ` 49 | 50 | vert 51 | ? styles += ` 52 | width: 1px; 53 | height: 100vh; 54 | transform: rotate(180deg); 55 | ` 56 | : styles += ` 57 | height: 1px; 58 | width: 100vw; 59 | ` 60 | 61 | guide.style = styles 62 | 63 | return guide 64 | } 65 | 66 | const stickGuide = els => { 67 | if (!els.length) return 68 | 69 | if (state.stuck.count >= els.length) { 70 | state.stuck.measurements.forEach(el => el.remove()) 71 | state.stuck.measurements = [] 72 | state.stuck.count = 0 73 | } 74 | 75 | state.stuck.count++ 76 | 77 | if (els.length > 1) { 78 | state.stuck.measurements = [ 79 | ...state.stuck.measurements, 80 | ...takeMeasurementOwnership(), 81 | ] 82 | } 83 | 84 | state.gridlines && state.gridlines.remove() 85 | state.gridlines = null 86 | } 87 | 88 | const on_hoverout = () => 89 | hideGridlines() 90 | 91 | const showGridlines = node => { 92 | if (state.gridlines) { 93 | state.gridlines.style.display = null 94 | state.gridlines.update = node.getBoundingClientRect() 95 | } 96 | else { 97 | state.gridlines = document.createElement('visbug-gridlines') 98 | state.gridlines.position = node.getBoundingClientRect() 99 | 100 | document.body.appendChild(state.gridlines) 101 | } 102 | } 103 | 104 | const hideGridlines = () => { 105 | if (!state.gridlines) return 106 | state.gridlines.style.display = 'none' 107 | } 108 | -------------------------------------------------------------------------------- /app/features/guides.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab } 4 | from '../../tests/helpers' 5 | 6 | test.beforeEach(setupPptrTab) 7 | 8 | test('Should show 1 overlay element on hover', async t => { 9 | const { page } = t.context 10 | 11 | await page.mouse.move(100, 200) 12 | 13 | const gridlines_element = await page.evaluate(`document.querySelectorAll('visbug-gridlines').length`) 14 | 15 | t.is(gridlines_element, 1) 16 | t.pass() 17 | }) 18 | 19 | test.afterEach(teardownPptrTab) 20 | -------------------------------------------------------------------------------- /app/features/index.js: -------------------------------------------------------------------------------- 1 | export { Margin } from './margin' 2 | export { Selectable } from './selectable' 3 | export { Moveable } from './move' 4 | export { Padding } from './padding' 5 | export { EditText } from './text' 6 | export { Font } from './font' 7 | export { Flex } from './flex' 8 | export { Search } from './search' 9 | export { ColorPicker } from './color' 10 | export { MetaTip } from './metatip' 11 | export { BoxShadow } from './boxshadow' 12 | export { HueShift } from './hueshift' 13 | export { Guides } from './guides' 14 | export { Screenshot } from './screenshot' 15 | export { Position, draggable } from './position' 16 | export { Accessibility } from './accessibility' 17 | 18 | -------------------------------------------------------------------------------- /app/features/margin.js: -------------------------------------------------------------------------------- 1 | import hotkeys from 'hotkeys-js' 2 | import { metaKey, getStyle, getSide, showHideSelected } from '../utilities/' 3 | 4 | const key_events = 'up,down,left,right' 5 | .split(',') 6 | .reduce((events, event) => 7 | `${events},${event},alt+${event},shift+${event},shift+alt+${event}` 8 | , '') 9 | .substring(1) 10 | 11 | const command_events = `${metaKey}+up,${metaKey}+shift+up,${metaKey}+down,${metaKey}+shift+down` 12 | 13 | export function Margin(visbug) { 14 | hotkeys(key_events, (e, handler) => { 15 | if (e.cancelBubble) return 16 | 17 | e.preventDefault() 18 | pushElement(visbug.selection(), handler.key) 19 | }) 20 | 21 | hotkeys(command_events, (e, handler) => { 22 | e.preventDefault() 23 | pushAllElementSides(visbug.selection(), handler.key) 24 | }) 25 | 26 | visbug.onSelectedUpdate(paintBackgrounds) 27 | 28 | return () => { 29 | hotkeys.unbind(key_events) 30 | hotkeys.unbind(command_events) 31 | hotkeys.unbind('up,down,left,right') // bug in lib? 32 | visbug.removeSelectedCallback(paintBackgrounds) 33 | removeBackgrounds(visbug.selection()) 34 | } 35 | } 36 | 37 | export function pushElement(els, direction) { 38 | els 39 | .map(el => showHideSelected(el)) 40 | .map(el => ({ 41 | el, 42 | style: 'margin' + getSide(direction), 43 | current: parseInt(getStyle(el, 'margin' + getSide(direction)), 10), 44 | amount: direction.split('+').includes('shift') ? 10 : 1, 45 | negative: direction.split('+').includes('alt'), 46 | })) 47 | .map(payload => 48 | Object.assign(payload, { 49 | margin: payload.negative 50 | ? payload.current - payload.amount 51 | : payload.current + payload.amount 52 | })) 53 | .forEach(({el, style, margin}) => 54 | el.style[style] = `${margin < 0 ? 0 : margin}px`) 55 | } 56 | 57 | export function pushAllElementSides(els, keycommand) { 58 | const combo = keycommand.split('+') 59 | let spoof = '' 60 | 61 | if (combo.includes('shift')) spoof = 'shift+' + spoof 62 | if (combo.includes('down')) spoof = 'alt+' + spoof 63 | 64 | 'up,down,left,right'.split(',') 65 | .forEach(side => pushElement(els, spoof + side)) 66 | } 67 | 68 | function paintBackgrounds(els) { 69 | els.forEach(el => { 70 | const label_id = el.getAttribute('data-label-id') 71 | 72 | document 73 | .querySelector(`visbug-handles[data-label-id="${label_id}"]`) 74 | .backdrop = { 75 | element: createMarginVisual(el), 76 | update: createMarginVisual, 77 | } 78 | }) 79 | } 80 | 81 | function removeBackgrounds(els) { 82 | els.forEach(el => { 83 | const label_id = el.getAttribute('data-label-id') 84 | const boxmodel = document.querySelector(`visbug-handles[data-label-id="${label_id}"]`) 85 | .$shadow.querySelector('visbug-boxmodel') 86 | 87 | if (boxmodel) boxmodel.remove() 88 | }) 89 | } 90 | 91 | export function createMarginVisual(el, hover = false) { 92 | const bounds = el.getBoundingClientRect() 93 | const calculatedStyle = getStyle(el, 'margin') 94 | const boxdisplay = document.createElement('visbug-boxmodel') 95 | 96 | if (calculatedStyle !== '0px') { 97 | const sides = { 98 | top: getStyle(el, 'marginTop'), 99 | right: getStyle(el, 'marginRight'), 100 | bottom: getStyle(el, 'marginBottom'), 101 | left: getStyle(el, 'marginLeft'), 102 | } 103 | 104 | Object.entries(sides).forEach(([side, val]) => { 105 | if (typeof val !== 'number') 106 | val = parseInt(getStyle(el, 'margin'+'-'+side).slice(0, -2)) 107 | 108 | sides[side] = Math.round(val.toFixed(1) * 100) / 100 109 | }) 110 | 111 | boxdisplay.position = { 112 | mode: 'margin', 113 | color: hover ? 'purple' : 'pink', 114 | bounds, 115 | sides, 116 | } 117 | } 118 | 119 | return boxdisplay 120 | } 121 | -------------------------------------------------------------------------------- /app/features/margin.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'margin' 7 | const test_selector = '[intro] b' 8 | 9 | const getMarginTop = async page => 10 | await page.$eval(test_selector, el => 11 | el.style.marginTop) 12 | 13 | test.beforeEach(async t => { 14 | await setupPptrTab(t) 15 | 16 | await changeMode({ 17 | tool, 18 | page: t.context.page, 19 | }) 20 | }) 21 | 22 | test('Can Be Activated', async t => { 23 | const { page } = t.context 24 | t.is(await getActiveTool(page), tool) 25 | t.pass() 26 | }) 27 | 28 | test('Can Be Deactivated', async t => { 29 | const { page } = t.context 30 | 31 | t.is(await getActiveTool(page), tool) 32 | await changeMode({ tool: 'padding', page }) 33 | t.is(await getActiveTool(page), 'padding') 34 | 35 | t.pass() 36 | }) 37 | 38 | test('Adds margin to side', async t => { 39 | const { page } = t.context 40 | 41 | await page.click(test_selector) 42 | 43 | t.is(await getMarginTop(page), '') 44 | 45 | await page.keyboard.press('ArrowUp') 46 | 47 | t.is(await getMarginTop(page), '1px') 48 | 49 | t.pass() 50 | }) 51 | 52 | test('Remove margin from side', async t => { 53 | const { page } = t.context 54 | 55 | await page.click(test_selector) 56 | t.is(await getMarginTop(page), '') 57 | 58 | await page.keyboard.press('ArrowUp') 59 | t.is(await getMarginTop(page), '1px') 60 | 61 | await page.keyboard.down('Alt') 62 | await page.keyboard.down('ArrowUp') 63 | await page.keyboard.up('Alt') 64 | await page.keyboard.up('ArrowUp') 65 | t.is(await getMarginTop(page), '0px') 66 | 67 | t.pass() 68 | }) 69 | 70 | test('Can change values by 10 with shift key', async t => { 71 | const { page } = t.context 72 | 73 | await page.click(test_selector) 74 | t.is(await getMarginTop(page), '') 75 | 76 | await page.keyboard.down('Shift') 77 | await page.keyboard.press('ArrowUp') 78 | await page.keyboard.up('Shift') 79 | t.is(await getMarginTop(page), '10px') 80 | 81 | t.pass() 82 | }) 83 | 84 | test.afterEach(teardownPptrTab) 85 | -------------------------------------------------------------------------------- /app/features/metatip.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'inspector' 7 | const test_selector = '[intro] b' 8 | 9 | test.beforeEach(async t => { 10 | await setupPptrTab(t) 11 | 12 | await changeMode({ 13 | tool, 14 | page: t.context.page, 15 | }) 16 | }) 17 | 18 | test('Can Be Activated', async t => { 19 | const { page } = t.context 20 | t.is(await getActiveTool(page), tool) 21 | t.pass() 22 | }) 23 | 24 | test('Can Be Deactivated', async t => { 25 | const { page } = t.context 26 | 27 | t.is(await getActiveTool(page), tool) 28 | await changeMode({ tool: 'padding', page }) 29 | t.is(await getActiveTool(page), 'padding') 30 | 31 | t.pass() 32 | }) 33 | 34 | test('Should show 1 metatip on click', async t => { 35 | const { page } = t.context 36 | 37 | await page.click(test_selector) 38 | const metatip_element = await page.evaluate(`document.querySelectorAll('visbug-metatip').length`) 39 | 40 | t.is(metatip_element, 1) 41 | t.pass() 42 | }) 43 | 44 | test('Should show tag name in header', async t => { 45 | const { page } = t.context 46 | 47 | await page.click(test_selector) 48 | const metatip_header_tag = await page.evaluate( 49 | `document.querySelector('visbug-metatip').$shadow.querySelector('figure > header a[node]').textContent` 50 | ) 51 | 52 | t.is(metatip_header_tag, 'b') 53 | t.pass() 54 | }) 55 | 56 | test.afterEach(teardownPptrTab) 57 | -------------------------------------------------------------------------------- /app/features/move.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'move' 7 | const test_selector = '[intro] b' 8 | 9 | const getNodeIndex = async (page, selector) => 10 | await page.$eval(selector, el => 11 | [...el.parentNode.children].indexOf(el)) 12 | 13 | test.beforeEach(async t => { 14 | await setupPptrTab(t) 15 | 16 | await changeMode({ 17 | tool, 18 | page: t.context.page, 19 | }) 20 | }) 21 | 22 | test('Can Be Activated', async t => { 23 | const { page } = t.context 24 | t.is(await getActiveTool(page), tool) 25 | t.pass() 26 | }) 27 | 28 | test('Can Be Deactivated', async t => { 29 | const { page } = t.context 30 | 31 | t.is(await getActiveTool(page), tool) 32 | await changeMode({ tool: 'padding', page }) 33 | t.is(await getActiveTool(page), 'padding') 34 | 35 | t.pass() 36 | }) 37 | 38 | test('Move sibling up the branch', async t => { 39 | const { page } = t.context 40 | 41 | await page.click(test_selector) 42 | t.is(await getNodeIndex(page, test_selector), 2) 43 | 44 | await page.keyboard.press('ArrowLeft') 45 | 46 | t.is(await getNodeIndex(page, test_selector), 1) 47 | 48 | t.pass() 49 | }) 50 | 51 | test('Move sibling down the branch', async t => { 52 | const { page } = t.context 53 | const alt_selector = '[intro] em' 54 | 55 | await page.click(alt_selector) 56 | t.is(await getNodeIndex(page, alt_selector), 0) 57 | 58 | await page.keyboard.press('ArrowRight') 59 | 60 | t.is(await getNodeIndex(page, alt_selector), 1) 61 | 62 | t.pass() 63 | }) 64 | 65 | test('Grips overlay siblings when 1 is selected', async t => { 66 | const { page } = t.context 67 | 68 | await page.click(test_selector) 69 | 70 | const grips_count = await page.evaluate('document.querySelectorAll("visbug-grip").length') 71 | 72 | t.is(grips_count, 3) 73 | 74 | t.pass() 75 | }) 76 | 77 | test('Drag bounds are highlighted', async t => { 78 | const { page } = t.context 79 | 80 | await page.click(test_selector) 81 | 82 | const bounds_count = await page.evaluate('document.querySelectorAll("[visbug-drag-container]").length') 83 | 84 | t.is(bounds_count, 1) 85 | 86 | t.pass() 87 | }) 88 | 89 | test.afterEach(teardownPptrTab) 90 | -------------------------------------------------------------------------------- /app/features/padding.js: -------------------------------------------------------------------------------- 1 | import hotkeys from 'hotkeys-js' 2 | import { metaKey, getStyle, getSide, showHideSelected, expandBorders } from '../utilities/' 3 | 4 | const key_events = 'up,down,left,right' 5 | .split(',') 6 | .reduce((events, event) => 7 | `${events},${event},alt+${event},shift+${event},shift+alt+${event}` 8 | , '') 9 | .substring(1) 10 | 11 | const command_events = `${metaKey}+up,${metaKey}+shift+up,${metaKey}+down,${metaKey}+shift+down` 12 | 13 | export function Padding(visbug) { 14 | hotkeys(key_events, (e, handler) => { 15 | if (e.cancelBubble) return 16 | 17 | e.preventDefault() 18 | padElement(visbug.selection(), handler.key) 19 | }) 20 | 21 | hotkeys(command_events, (e, handler) => { 22 | e.preventDefault() 23 | padAllElementSides(visbug.selection(), handler.key) 24 | }) 25 | 26 | visbug.onSelectedUpdate(paintBackgrounds) 27 | 28 | return () => { 29 | hotkeys.unbind(key_events) 30 | hotkeys.unbind(command_events) 31 | hotkeys.unbind('up,down,left,right') // bug in lib? 32 | visbug.removeSelectedCallback(paintBackgrounds) 33 | removeBackgrounds(visbug.selection()) 34 | } 35 | } 36 | 37 | export function padElement(els, direction) { 38 | els 39 | .map(el => showHideSelected(el)) 40 | .map(el => ({ 41 | el, 42 | style: 'padding' + getSide(direction), 43 | current: parseInt(getStyle(el, 'padding' + getSide(direction)), 10), 44 | amount: direction.split('+').includes('shift') ? 10 : 1, 45 | negative: direction.split('+').includes('alt'), 46 | })) 47 | .map(payload => 48 | Object.assign(payload, { 49 | padding: payload.negative 50 | ? payload.current - payload.amount 51 | : payload.current + payload.amount 52 | })) 53 | .forEach(({el, style, padding}) => 54 | el.style[style] = `${padding < 0 ? 0 : padding}px`) 55 | } 56 | 57 | export function padAllElementSides(els, keycommand) { 58 | const combo = keycommand.split('+') 59 | let spoof = '' 60 | 61 | if (combo.includes('shift')) spoof = 'shift+' + spoof 62 | if (combo.includes('down')) spoof = 'alt+' + spoof 63 | 64 | 'up,down,left,right'.split(',') 65 | .forEach(side => padElement(els, spoof + side)) 66 | } 67 | 68 | function paintBackgrounds(els) { 69 | els.forEach(el => { 70 | const label_id = el.getAttribute('data-label-id') 71 | 72 | document 73 | .querySelector(`visbug-handles[data-label-id="${label_id}"]`) 74 | .backdrop = { 75 | element: createPaddingVisual(el), 76 | update: createPaddingVisual, 77 | } 78 | }) 79 | } 80 | 81 | function removeBackgrounds(els) { 82 | els.forEach(el => { 83 | const label_id = el.getAttribute('data-label-id') 84 | const boxmodel = document.querySelector(`visbug-handles[data-label-id="${label_id}"]`) 85 | .$shadow.querySelector('visbug-boxmodel') 86 | 87 | if (boxmodel) boxmodel.remove() 88 | }) 89 | } 90 | 91 | export function createPaddingVisual(el, hover = false) { 92 | const bounds = el.getBoundingClientRect() 93 | const calculatedStyle = getStyle(el, 'padding') 94 | const calculatedBorder = expandBorders(getStyle(el, 'border-width')) 95 | const boxdisplay = document.createElement('visbug-boxmodel') 96 | 97 | if (calculatedStyle !== '0px') { 98 | const sides = { 99 | top: getStyle(el, 'paddingTop'), 100 | right: getStyle(el, 'paddingRight'), 101 | bottom: getStyle(el, 'paddingBottom'), 102 | left: getStyle(el, 'paddingLeft'), 103 | } 104 | 105 | Object.entries(sides).forEach(([side, val]) => { 106 | if (typeof val !== 'number') 107 | val = parseInt(getStyle(el, 'padding'+'-'+side).slice(0, -2)) 108 | 109 | sides[side] = Math.round(val.toFixed(1) * 100) / 100 110 | }) 111 | 112 | boxdisplay.position = { 113 | mode: 'padding', 114 | color: hover ? 'purple' : 'pink', 115 | bounds, 116 | sides: { 117 | ...sides, 118 | borders: calculatedBorder, 119 | }, 120 | } 121 | } 122 | 123 | return boxdisplay 124 | } 125 | -------------------------------------------------------------------------------- /app/features/padding.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'padding' 7 | const test_selector = '[intro] b' 8 | 9 | const getPaddingTop = async page => 10 | await page.$eval(test_selector, el => 11 | el.style.paddingTop) 12 | 13 | test.beforeEach(async t => { 14 | await setupPptrTab(t) 15 | 16 | await changeMode({ 17 | tool, 18 | page: t.context.page, 19 | }) 20 | }) 21 | 22 | test('Can Be Activated', async t => { 23 | const { page } = t.context 24 | t.is(await getActiveTool(page), tool) 25 | t.pass() 26 | }) 27 | 28 | test('Can Be Deactivated', async t => { 29 | const { page } = t.context 30 | 31 | t.is(await getActiveTool(page), tool) 32 | await changeMode({ tool: 'margin', page }) 33 | t.is(await getActiveTool(page), 'margin') 34 | 35 | t.pass() 36 | }) 37 | 38 | test('Adds padding to side', async t => { 39 | const { page } = t.context 40 | 41 | await page.click(test_selector) 42 | 43 | t.is(await getPaddingTop(page), '') 44 | 45 | await page.keyboard.press('ArrowUp') 46 | 47 | t.is(await getPaddingTop(page), '1px') 48 | 49 | t.pass() 50 | }) 51 | 52 | test('Remove padding from side', async t => { 53 | const { page } = t.context 54 | 55 | await page.click(test_selector) 56 | t.is(await getPaddingTop(page), '') 57 | 58 | await page.keyboard.press('ArrowUp') 59 | t.is(await getPaddingTop(page), '1px') 60 | 61 | await page.keyboard.down('Alt') 62 | await page.keyboard.down('ArrowUp') 63 | await page.keyboard.up('Alt') 64 | await page.keyboard.up('ArrowUp') 65 | t.is(await getPaddingTop(page), '0px') 66 | 67 | t.pass() 68 | }) 69 | 70 | test('Can change values by 10 with shift key', async t => { 71 | const { page } = t.context 72 | 73 | await page.click(test_selector) 74 | t.is(await getPaddingTop(page), '') 75 | 76 | await page.keyboard.down('Shift') 77 | await page.keyboard.press('ArrowUp') 78 | await page.keyboard.up('Shift') 79 | t.is(await getPaddingTop(page), '10px') 80 | 81 | t.pass() 82 | }) 83 | 84 | test.afterEach(teardownPptrTab) 85 | -------------------------------------------------------------------------------- /app/features/position.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { setupPptrTab, teardownPptrTab, changeMode, getActiveTool } 4 | from '../../tests/helpers' 5 | 6 | const tool = 'position' 7 | const test_selector = '[intro] h1' 8 | 9 | test.beforeEach(async t => { 10 | await setupPptrTab(t) 11 | 12 | await changeMode({ 13 | tool, 14 | page: t.context.page, 15 | }) 16 | }) 17 | 18 | test('Can Be Activated', async t => { 19 | const { page } = t.context 20 | t.is(await getActiveTool(page), tool) 21 | t.pass() 22 | }) 23 | 24 | test('Test Nudge Up/Down Works', async t => { 25 | const { page } = t.context 26 | 27 | const originalPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 28 | await page.click(test_selector) 29 | 30 | await page.keyboard.press('ArrowUp') 31 | const changedPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 32 | t.true(originalPageTop - 1 === changedPageTop) 33 | 34 | await page.keyboard.press('ArrowDown') 35 | await page.keyboard.press('ArrowDown') 36 | const finalPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 37 | t.true(originalPageTop + 1 === finalPageTop) 38 | 39 | t.pass() 40 | }) 41 | 42 | test('Test Nudge Left/Right Works', async t => { 43 | const { page } = t.context 44 | 45 | const originalPageLeft = await page.$eval(test_selector, el => el.getBoundingClientRect().left) 46 | await page.click(test_selector) 47 | 48 | await page.keyboard.press('ArrowLeft') 49 | const changedPageLeft = await page.$eval(test_selector, el => el.getBoundingClientRect().left) 50 | t.true(originalPageLeft - 1 === changedPageLeft) 51 | 52 | await page.keyboard.press('ArrowRight') 53 | await page.keyboard.press('ArrowRight') 54 | const finalPageLeft = await page.$eval(test_selector, el => el.getBoundingClientRect().left) 55 | t.true(originalPageLeft + 1 === finalPageLeft) 56 | 57 | t.pass() 58 | }) 59 | 60 | test('Test Shift Nudge Up/Down Works', async t => { 61 | const { page } = t.context 62 | 63 | const originalPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 64 | await page.click(test_selector) 65 | await page.keyboard.down("Shift") 66 | await page.keyboard.press('ArrowUp') 67 | const changedPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 68 | t.true(originalPageTop - 10 === changedPageTop) 69 | t.pass() 70 | }) 71 | 72 | 73 | test('Test Drag Works', async t => { 74 | const { page } = t.context 75 | 76 | const { originalTop, originalLeft } = await page.$eval(test_selector, el => { 77 | return { 78 | originalTop : el.getBoundingClientRect().top, 79 | originalLeft : el.getBoundingClientRect().left 80 | } 81 | }) 82 | 83 | await page.click(test_selector) 84 | 85 | await page.mouse.down() 86 | await page.mouse.move(20,20) 87 | const changedPageTop = await page.$eval(test_selector, el => el.getBoundingClientRect().top) 88 | const changedPageLeft = await page.$eval(test_selector, el => el.getBoundingClientRect().left) 89 | 90 | t.true(changedPageTop + 50 < originalTop) 91 | t.true(changedPageLeft + 50 < originalLeft) 92 | 93 | t.pass() 94 | }) 95 | 96 | 97 | 98 | test.afterEach(teardownPptrTab) 99 | -------------------------------------------------------------------------------- /app/features/screenshot.js: -------------------------------------------------------------------------------- 1 | export function Screenshot(node, page) { 2 | alert('Coming Soon!') 3 | 4 | return () => {} 5 | } -------------------------------------------------------------------------------- /app/features/search.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import hotkeys from 'hotkeys-js' 3 | import { querySelectorAllDeep } from 'query-selector-shadow-dom' 4 | import { PluginRegistry, PluginHints } from '../plugins/_registry' 5 | import { notList } from '../utilities' 6 | import { isFirefox } from '../utilities/cross-browser.js' 7 | 8 | let SelectorEngine 9 | 10 | // create input 11 | const search_base = document.createElement('div') 12 | search_base.classList.add('search') 13 | search_base.innerHTML = ` 14 | 15 | 16 | ${isFirefox > 0 17 | ? `
` 24 | 25 | : ` 26 | 27 | 28 | 29 | 30 | 31 |
`} 32 | 33 | ${PluginHints.reduce((options, hint) => 34 | options += isFirefox > 0 35 | ? ``) 117 | .join('') 118 | 119 | if (!query.includes('colorblind')) 120 | node.querySelector(`#${query}`) 121 | .selected = 'selected' 122 | 123 | node.style = ` 124 | position: fixed; 125 | top: 10px; 126 | right: 10px; 127 | z-index: 999999999; 128 | ` 129 | 130 | node.setAttribute('size', types.length) 131 | 132 | node.addEventListener('input', e => 133 | document.body.style.filter = `url(#${e.target.value})`) 134 | 135 | return node 136 | } 137 | 138 | export default async function({selected, query}) { 139 | query = query.slice(1, query.length) 140 | 141 | // only inject filters once 142 | if (!state.filters_injected) { 143 | const filters = makeFilterSVGNode() 144 | const select = makeSelectMenu(query) 145 | 146 | document.body.appendChild(filters) 147 | document.body.appendChild(select) 148 | 149 | state.filters_injected = true 150 | } 151 | 152 | query.includes('colorblind') 153 | ? document.body.style.filter = `url(#${types[0]})` 154 | : document.body.style.filter = `url(#${query})` 155 | } 156 | -------------------------------------------------------------------------------- /app/plugins/construct.debug.js: -------------------------------------------------------------------------------- 1 | import { loadStyles } from '../utilities/styles.js' 2 | 3 | export const commands = [ 4 | 'debug trashy', 5 | 'debug construct', 6 | ] 7 | 8 | export const description = 'visualize the semantic structure of the page (loads construct.css)' 9 | 10 | export default async function() { 11 | await loadStyles(['https://cdn.jsdelivr.net/gh/t7/construct.css@master/css/construct.debug.css']) 12 | } -------------------------------------------------------------------------------- /app/plugins/construct.js: -------------------------------------------------------------------------------- 1 | import { loadStyles } from '../utilities/styles.js' 2 | 3 | export const commands = [ 4 | 'trashy', 5 | 'construct', 6 | ] 7 | 8 | export const description = 'visualize the semantic structure of the page (loads construct.css)' 9 | 10 | export default async function() { 11 | await loadStyles(['https://cdn.jsdelivr.net/gh/t7/construct.css@master/css/construct.boxes.css']) 12 | } -------------------------------------------------------------------------------- /app/plugins/ct-head-scan.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'head scan', 3 | ] 4 | 5 | export const description = `diagnose potential performance issues in your page's tags` 6 | 7 | export default function () { 8 | const ct = document.createElement("link"); 9 | ct.rel = "stylesheet"; 10 | ct.href = "https://csswizardry.com/ct/ct.css"; 11 | ct.classList.add("ct"); 12 | document.head.appendChild(ct); 13 | } -------------------------------------------------------------------------------- /app/plugins/detect-overflows.js: -------------------------------------------------------------------------------- 1 | import { 2 | isFixed, 3 | colors, 4 | numberBetween 5 | } from '../utilities/' 6 | 7 | export const commands = [ 8 | 'detect overflows', 9 | 'overflow', 10 | ] 11 | 12 | export const description = 'find elements that overflow the page body' 13 | 14 | export default (elements) => { 15 | let selectedElements; 16 | if (elements && elements.selected.length) { 17 | selectedElements = elements.selected 18 | } else { 19 | selectedElements = [document.body] 20 | } 21 | 22 | document.body.querySelectorAll('visbug-label') 23 | .forEach((el) => el.remove()) 24 | 25 | selectedElements.map(container => { 26 | const elementsToCheck = container.querySelectorAll('*') 27 | elementsToCheck.forEach(el => { 28 | const overflowingX = el.offsetWidth > container.offsetWidth 29 | const overflowingY = el.offsetHeight > container.offsetHeight 30 | const isFlag = el.tagName === 'visbug-label' 31 | const alreadyHasFlag = el.lastChild && el.lastChild.tagName === 'visbug-label' 32 | if ((overflowingX || overflowingY) && !isFlag && !alreadyHasFlag) { 33 | const label = document.createElement('visbug-label') 34 | const overflowingBoth = overflowingX && overflowingY; 35 | label.text = `overflowing ${overflowingBoth ? 'x and y' : overflowingX ? 'x' : 'y'}` 36 | label.position = { 37 | boundingRect: el.getBoundingClientRect(), 38 | isFixed: isFixed(el), 39 | } 40 | const color = colors[numberBetween(0, colors.length)] 41 | label.style.setProperty('--label-bg', color) 42 | container.appendChild(label) 43 | el.style.setProperty('outline', `1px solid ${color}`) 44 | } 45 | }) 46 | }) 47 | } -------------------------------------------------------------------------------- /app/plugins/loop-through-widths.js: -------------------------------------------------------------------------------- 1 | // https://github.com/hchiam/learning-js/blob/master/bookmarklets/autoResizeWindowToTestMediaQueries.js 2 | 3 | export const commands = [ 4 | 'loop through widths', 5 | 'loop thru widths', 6 | ] 7 | 8 | export const description = 'loops through screen widths in a popup' 9 | 10 | export default async function() { 11 | 12 | const test = (function () { 13 | let keepGoing = true; 14 | let popup; 15 | const maxWidth = screen.width; 16 | const minWidth = 152; 17 | let goWider = true; 18 | 19 | openPopup(); 20 | go(); 21 | 22 | popup.onbeforeunload = function () { 23 | stop(); 24 | console.log("Stopped timer."); 25 | }; 26 | 27 | function openPopup() { 28 | popup = window.open(location.href, "_blank", "width=100, top=0, left=0"); 29 | } 30 | 31 | function wider() { 32 | popup.resizeBy(5, 0); 33 | } 34 | 35 | function thinner() { 36 | popup.resizeBy(-5, 0); 37 | } 38 | 39 | function stop() { 40 | keepGoing = false; 41 | } 42 | 43 | function go() { 44 | keepGoing = true; 45 | console.log("Popup will start looping thru screen widths in 3 seconds."); 46 | popup.focus(); 47 | setTimeout(function () { 48 | scanWidths(); 49 | const codeStyle = 'background: black; color: lime;' 50 | const resetStyle = 'background: inherit; color: inherit;' 51 | console.log( 52 | "You can run %ctest.stop()%c to stop, and \n%ctest.go()%c to continue.", 53 | codeStyle, resetStyle, codeStyle, resetStyle 54 | ); 55 | }, 3000); 56 | } 57 | 58 | function scanWidths() { 59 | const timer = setInterval(function () { 60 | if (!keepGoing) { 61 | clearTimeout(timer); 62 | popup.focus(); 63 | } 64 | if (maxWidth <= popup.outerWidth) { 65 | goWider = false; 66 | } else if (popup.outerWidth <= minWidth) { 67 | goWider = true; 68 | } 69 | if (goWider) { 70 | wider(); 71 | } else { 72 | thinner(); 73 | } 74 | }, 100); 75 | } 76 | 77 | return { 78 | stop, 79 | go, 80 | popup 81 | }; 82 | 83 | })(); 84 | 85 | if (typeof window !== "undefined") window.test = test; 86 | } -------------------------------------------------------------------------------- /app/plugins/no-mouse-days.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'no mouse days', 3 | 'disable mouse', 4 | ] 5 | 6 | export const description = 'disable your mouse cursor (e.g. to test keyboard-only support for accessibility)' 7 | 8 | export default async function() { 9 | await import('https://unpkg.com/no-mouse-days@latest') 10 | window.onload() 11 | } 12 | -------------------------------------------------------------------------------- /app/plugins/pesticide.js: -------------------------------------------------------------------------------- 1 | import { loadStyles } from '../utilities/styles.js' 2 | 3 | export const commands = [ 4 | 'pesticide', 5 | ] 6 | 7 | export const description = 'show where the outlines of all elements are, to debug layout' 8 | 9 | export default async function() { 10 | await loadStyles(['https://unpkg.com/pesticide@1.3.1/css/pesticide.min.css']) 11 | } -------------------------------------------------------------------------------- /app/plugins/placeholdifier.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'placeholdifier', 3 | ] 4 | 5 | export const description = 'turn the page into a live wireframe' 6 | 7 | export default async function() { 8 | const stylesheet = document.createElement('link') 9 | stylesheet.setAttribute('rel', 'stylesheet') 10 | stylesheet.setAttribute('href', 'https://unpkg.com/placeholdifier/placeholdifier.css') 11 | document.head.appendChild(stylesheet) 12 | 13 | const ignored = ['script', 'link'] 14 | const body = document.querySelector('body') 15 | const elements = Array.from(body.querySelectorAll('*')) 16 | .filter(el => { 17 | if (!el || !el.tagName) return 18 | return !ignored.includes(el.tagName.toLowerCase()) 19 | }) 20 | 21 | elements.forEach(el => { 22 | if (Array.from(el.classList).includes('placeholdify')) return; 23 | el.classList.add('placeholdify'); 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /app/plugins/remove-css.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'remove css', 3 | 'disable css', 4 | ] 5 | 6 | export const description = 'remove the style and link tags from the page' 7 | 8 | export default function () { 9 | [ 10 | ...document.querySelectorAll('style'), 11 | ...document.querySelectorAll('link'), 12 | ].forEach((el) => el.remove()) 13 | 14 | document 15 | .querySelectorAll('[style]:not(vis-bug)') 16 | .forEach((el) => el.removeAttribute('style')) 17 | } 18 | -------------------------------------------------------------------------------- /app/plugins/revenge.js: -------------------------------------------------------------------------------- 1 | // http://heydonworks.com/revenge_css_bookmarklet/ 2 | 3 | import { loadStyles } from '../utilities/styles.js' 4 | 5 | export const commands = [ 6 | 'revenge', 7 | 'revenge.css', 8 | 'heydon', 9 | ] 10 | 11 | export const description = 'show error boxes in comic sans that point out bad HTML' 12 | 13 | export default async function() { 14 | await loadStyles(['https://cdn.jsdelivr.net/gh/Heydon/REVENGE.CSS@master/revenge.css']) 15 | } -------------------------------------------------------------------------------- /app/plugins/shuffle.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'shuffle', 3 | ] 4 | 5 | export const description = 'shuffle the direct children of the currently-selected element' 6 | 7 | export default async (selectedElement) => { 8 | const getSiblings = (elem) => { 9 | // Setup siblings array and get the first sibling 10 | let siblings = []; 11 | let sibling = elem.firstChild; 12 | // Loop through each sibling and push to the array 13 | while (sibling) { 14 | if (sibling.nodeType === 1 && sibling !== elem) { 15 | siblings.push(sibling); 16 | } 17 | sibling = sibling.nextSibling 18 | } 19 | return siblings; 20 | }; 21 | const shuffle = (array) => { 22 | let currentIndex = array.length, temporaryValue, randomIndex; 23 | 24 | // While there remain elements to shuffle... 25 | while (0 !== currentIndex) { 26 | 27 | // Pick a remaining element... 28 | randomIndex = Math.floor(Math.random() * currentIndex); 29 | currentIndex -= 1; 30 | 31 | // And swap it with the current element. 32 | temporaryValue = array[currentIndex]; 33 | array[currentIndex] = array[randomIndex]; 34 | array[randomIndex] = temporaryValue; 35 | } 36 | return array; 37 | }; 38 | const appendShuffledSiblings = (element, shuffledElementsArray) => { 39 | element.innerHTML = ''; 40 | for (let i = 0; i < shuffledElementsArray.length; i++) { 41 | element.appendChild(shuffledElementsArray[i]) 42 | } 43 | }; 44 | const { selected } = selectedElement; 45 | selected.map(selectedElem => { 46 | const siblings = getSiblings(selectedElem); 47 | const shuffledSiblings = shuffle(siblings); 48 | appendShuffledSiblings(selectedElem, shuffledSiblings); 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /app/plugins/skeleton.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'skeleton', 3 | 'outline', 4 | ] 5 | 6 | export const description = 'turn everything into wireframe boxes' 7 | 8 | export default async function() { 9 | const styles = ` 10 | *:not(path):not(g) { 11 | color: hsl(0, 0%, 0%) !important; 12 | text-shadow: none !important; 13 | background: hsl(0, 0%, 100%) !important; 14 | outline: 1px solid hsla(0, 0%, 0%, 0.5) !important; 15 | border-color: transparent !important; 16 | box-shadow: none !important; 17 | } 18 | ` 19 | 20 | const style = document.createElement('style') 21 | style.textContent = styles 22 | document.head.appendChild(style) 23 | } -------------------------------------------------------------------------------- /app/plugins/tag-debugger.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/addyosmani/fd3999ea7fce242756b1 2 | export const commands = [ 3 | 'tag debugger', 4 | 'osmani', 5 | ] 6 | 7 | export const description = 'outline every DOM element with a random color, to visualize layout' 8 | 9 | export default async function() { 10 | let i, A; 11 | for (i = 0; A = document.querySelectorAll('*')[i++];) 12 | A.style.outline = `solid hsl(${(A+A).length*9},99%,50%) 1px` 13 | } -------------------------------------------------------------------------------- /app/plugins/tota11y.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'tota11y', 3 | ] 4 | 5 | export const description = 'inject the tota11y accessibility button on the bottom corner of the page' 6 | 7 | export default async function() { 8 | await import('https://cdnjs.cloudflare.com/ajax/libs/tota11y/0.1.6/tota11y.min.js') 9 | } 10 | -------------------------------------------------------------------------------- /app/plugins/wireframe.js: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'wireframe', 3 | 'blueprint', 4 | ] 5 | 6 | export const description = 'turn the page into a blueprint wireframe' 7 | 8 | export default async function() { 9 | const styles = ` 10 | *:not(path):not(g) { 11 | color: hsla(210, 100%, 100%, 0.9) !important; 12 | background: hsla(210, 100%, 50%, 0.5) !important; 13 | outline: solid 0.25rem hsla(210, 100%, 100%, 0.5) !important; 14 | box-shadow: none !important; 15 | } 16 | ` 17 | 18 | const style = document.createElement('style') 19 | style.textContent = styles 20 | document.head.appendChild(style) 21 | } -------------------------------------------------------------------------------- /app/plugins/zindex.js: -------------------------------------------------------------------------------- 1 | import { 2 | isFixed, 3 | colors, 4 | numberBetween 5 | } from '../utilities/' 6 | 7 | export const commands = [ 8 | 'zindex', 9 | 'z-index' 10 | ] 11 | 12 | export const description = 'show the z-index values of all elements that have z-index explicitly set (not auto)' 13 | 14 | export default function () { 15 | // Fun prior art https://gist.github.com/paulirish/211209 16 | Array.from(document.querySelectorAll('*')) 17 | .filter(el => window.getComputedStyle(el).getPropertyValue('z-index') !== 'auto') 18 | .filter(el => el.nodeName !== 'VIS-BUG') 19 | .forEach(el => { 20 | const color = colors[numberBetween(0, colors.length)]; 21 | const zindex = window.getComputedStyle(el).getPropertyValue('z-index') 22 | 23 | const label = document.createElement('visbug-label') 24 | 25 | label.text = `z-index: ${zindex}` 26 | label.position = { 27 | boundingRect: el.getBoundingClientRect(), 28 | isFixed: isFixed(el), 29 | } 30 | label.style.setProperty(`--label-bg`, color) 31 | 32 | const overlay = document.createElement('visbug-hover') 33 | overlay.position = { el } 34 | overlay.style.setProperty(`--hover-stroke`, color) 35 | overlay.style.setProperty(`--position`, isFixed(el) ? 'fixed' : 'absolute') 36 | 37 | document.body.appendChild(label) 38 | document.body.appendChild(overlay) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /app/tuts/accessibility.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/accessibility.gif -------------------------------------------------------------------------------- /app/tuts/align.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/align.gif -------------------------------------------------------------------------------- /app/tuts/boxshadow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/boxshadow.gif -------------------------------------------------------------------------------- /app/tuts/font.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/font.gif -------------------------------------------------------------------------------- /app/tuts/guides.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/guides.gif -------------------------------------------------------------------------------- /app/tuts/hueshift.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/hueshift.gif -------------------------------------------------------------------------------- /app/tuts/inspector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/inspector.gif -------------------------------------------------------------------------------- /app/tuts/margin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/margin.gif -------------------------------------------------------------------------------- /app/tuts/move.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/move.gif -------------------------------------------------------------------------------- /app/tuts/padding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/padding.gif -------------------------------------------------------------------------------- /app/tuts/position.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/position.gif -------------------------------------------------------------------------------- /app/tuts/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/search.gif -------------------------------------------------------------------------------- /app/tuts/text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/app/tuts/text.gif -------------------------------------------------------------------------------- /app/utilities/accessibility.js: -------------------------------------------------------------------------------- 1 | import { desiredAccessibilityMap, desiredPropMap, largeWCAG2TextMap } from './design-properties' 2 | import { getStyles } from './styles' 3 | 4 | export const getA11ys = el => { 5 | const elAttributes = el.getAttributeNames() 6 | 7 | return desiredAccessibilityMap.reduce((acc, attribute) => { 8 | if (elAttributes.includes(attribute)) 9 | acc.push({ 10 | prop: attribute, 11 | value: el.getAttribute(attribute) 12 | }) 13 | 14 | if (attribute === 'aria-*') 15 | elAttributes.forEach(attr => { 16 | if (attr.includes('aria')) 17 | acc.push({ 18 | prop: attr, 19 | value: el.getAttribute(attr) 20 | }) 21 | }) 22 | 23 | return acc 24 | }, []) 25 | } 26 | 27 | export const getWCAG2TextSize = el => { 28 | 29 | const styles = getStyles(el).reduce((styleMap, style) => { 30 | styleMap[style.prop] = style.value 31 | return styleMap 32 | }, {}) 33 | 34 | const { fontSize = desiredPropMap.fontSize, 35 | fontWeight = desiredPropMap.fontWeight 36 | } = styles 37 | 38 | const isLarge = largeWCAG2TextMap.some( 39 | (largeProperties) => parseFloat(fontSize) >= parseFloat(largeProperties.fontSize) 40 | && parseFloat(fontWeight) >= parseFloat(largeProperties.fontWeight) 41 | ) 42 | 43 | return isLarge ? 'Large' : 'Small' 44 | } -------------------------------------------------------------------------------- /app/utilities/colors.js: -------------------------------------------------------------------------------- 1 | import Color from 'colorjs.io' 2 | 3 | export const colors = ["#eb4034", "#30850f", "#116da7", "#4334eb", "#b134eb", "#df168e", "#e8172c", "#8f2e2b", "#8f692b", "#8a8f2b", "#358f2b", "#2b8f82", "#2b678f", "#2b2b8f", "#8f2b8f", "#8f2b55", "#1eff00", "#a86800", "#ff0000", "#008035", "#0026ff", "#bb00ff", "#d600b6", "#e60067", "#137878"]; 4 | 5 | export function contrast_color(c) { 6 | try { 7 | const color = new Color(c) 8 | 9 | const whContrast = color.contrastLstar('white') 10 | const blContrast = color.contrastLstar('black') 11 | 12 | return whContrast > blContrast ? 'white' : 'black' 13 | } catch {} 14 | } -------------------------------------------------------------------------------- /app/utilities/common.js: -------------------------------------------------------------------------------- 1 | import $ from 'blingblingjs' 2 | import { nodeKey } from './strings' 3 | 4 | export const deepElementFromPoint = (x, y) => { 5 | const el = document.elementFromPoint(x, y) 6 | 7 | const crawlShadows = node => { 8 | if (node.shadowRoot) { 9 | const potential = node.shadowRoot.elementFromPoint(x, y) 10 | 11 | if (potential == node) return node 12 | else if (potential.shadowRoot) return crawlShadows(potential) 13 | else return potential 14 | } 15 | else return node 16 | } 17 | 18 | const nested_shadow = crawlShadows(el) 19 | 20 | return nested_shadow || el 21 | } 22 | 23 | export const getSide = direction => { 24 | let start = direction.split('+').pop().replace(/^\w/, c => c.toUpperCase()) 25 | if (start == 'Up') start = 'Top' 26 | if (start == 'Down') start = 'Bottom' 27 | return start 28 | } 29 | 30 | export const getNodeIndex = el => { 31 | return [...el.parentElement.parentElement.children] 32 | .indexOf(el.parentElement) 33 | } 34 | 35 | export function showEdge(el) { 36 | return el.animate([ 37 | { outline: '1px solid transparent' }, 38 | { outline: '1px solid hsla(330, 100%, 71%, 80%)' }, 39 | { outline: '1px solid transparent' }, 40 | ], 600) 41 | } 42 | 43 | let timeoutMap = {} 44 | export const showHideSelected = (el, duration = 750) => { 45 | el.setAttribute('data-selected-hide', true) 46 | showHideNodeLabel(el, true) 47 | 48 | if (timeoutMap[nodeKey(el)]) 49 | clearTimeout(timeoutMap[nodeKey(el)]) 50 | 51 | timeoutMap[nodeKey(el)] = setTimeout(_ => { 52 | el.removeAttribute('data-selected-hide') 53 | showHideNodeLabel(el, false) 54 | }, duration) 55 | 56 | return el 57 | } 58 | 59 | export const showHideNodeLabel = (el, show = false) => { 60 | if (!el.hasAttribute('data-label-id')) 61 | return 62 | 63 | const label_id = el.getAttribute('data-label-id') 64 | 65 | const nodes = $(` 66 | visbug-label[data-label-id="${label_id}"], 67 | visbug-handles[data-label-id="${label_id}"] 68 | `) 69 | 70 | nodes.length && show 71 | ? nodes.forEach(el => 72 | el.style.display = 'none') 73 | : nodes.forEach(el => 74 | el.style.display = null) 75 | } 76 | 77 | export const htmlStringToDom = (htmlString = "") => 78 | (new DOMParser().parseFromString(htmlString, 'text/html')) 79 | .body.firstChild 80 | 81 | export const isOffBounds = node => 82 | node.closest && ( 83 | node.closest('vis-bug') 84 | || node.closest('hotkey-map') 85 | || node.closest('visbug-metatip') 86 | || node.closest('visbug-ally') 87 | || node.closest('visbug-label') 88 | || node.closest('visbug-handles') 89 | || node.closest('visbug-corners') 90 | || node.closest('visbug-grip') 91 | || node.closest('visbug-gridlines') 92 | ) 93 | 94 | export const isSelectorValid = (qs => ( 95 | selector => { 96 | try { qs(selector) } catch (e) { return false } 97 | return true 98 | } 99 | ))(s => document.createDocumentFragment().querySelector(s)) 100 | 101 | export const swapElements = (src, target) => { 102 | var temp = document.createElement("div") 103 | 104 | src.parentNode.insertBefore(temp, src) 105 | target.parentNode.insertBefore(src, target) 106 | temp.parentNode.insertBefore(target, temp) 107 | 108 | temp.parentNode.removeChild(temp) 109 | } 110 | 111 | export const onRemove = (element, callback) => { 112 | const parent = element.parentNode 113 | ? element.parentNode 114 | : document.body 115 | 116 | if (!parent) throw new Error("The node must already be attached") 117 | 118 | const obs = new MutationObserver(mutations => { 119 | for (const mutation of mutations) { 120 | for (const el of mutation.removedNodes) { 121 | if (el === element) { 122 | obs.disconnect() 123 | callback() 124 | } 125 | } 126 | } 127 | }) 128 | 129 | obs.observe(parent, { 130 | childList: true, 131 | }) 132 | } -------------------------------------------------------------------------------- /app/utilities/cross-browser.js: -------------------------------------------------------------------------------- 1 | export const isFirefox = navigator.userAgent.search('Firefox') > 0 2 | export const isSafarish = navigator.userAgent.search('Safari') > 0 3 | export const isChrome = navigator.userAgent.search('Chrome') > 0 4 | export const isSafari = isSafarish && !isChrome 5 | 6 | export const isPolyfilledCE = shadow_node => 7 | shadow_node.children.length === 1 && shadow_node.firstElementChild.nodeName === 'STYLE' 8 | ? true 9 | : false 10 | 11 | const testConstructible = () => { 12 | try { 13 | new window.CSSStyleSheet('a{}') 14 | return true 15 | } 16 | catch (e) { 17 | return false 18 | } 19 | } 20 | 21 | export const constructibleStylesheetSupport = testConstructible() 22 | -------------------------------------------------------------------------------- /app/utilities/design-properties.js: -------------------------------------------------------------------------------- 1 | import {isFirefox, isSafari} from './cross-browser.js' 2 | 3 | export const desiredPropMap = { 4 | color: 'rgb(0, 0, 0)', 5 | backgroundColor: 'rgba(0, 0, 0, 0)', 6 | backgroundImage: 'none', 7 | backgroundSize: 'auto', 8 | backgroundPosition: '0% 0%', 9 | borderRadius: '0px', 10 | boxShadow: 'none', 11 | padding: '0px', 12 | margin: '0px', 13 | fontFamily: 'auto', 14 | fontSize: '16px', 15 | fontWeight: '400', 16 | textAlign: 'start', 17 | textShadow: 'none', 18 | textTransform: 'none', 19 | lineHeight: 'normal', 20 | letterSpacing: 'normal', 21 | display: 'block', 22 | alignItems: 'normal', 23 | justifyContent: 'normal', 24 | flexDirection: 'row', 25 | flexWrap: 'nowrap', 26 | flexBasis: 'auto', 27 | // flexFlow: 'none', 28 | fill: 'rgb(0, 0, 0)', 29 | stroke: 'none', 30 | gridTemplateColumns: 'none', 31 | gridAutoColumns: 'auto', 32 | gridTemplateRows: 'none', 33 | gridAutoRows: 'auto', 34 | gridTemplateAreas: 'none', 35 | gridArea: 'auto', 36 | gap: 'normal', 37 | gridAutoFlow: 'row', 38 | } 39 | 40 | if (isFirefox) { 41 | desiredPropMap.backgroundSize = 'auto' 42 | desiredPropMap.borderWidth = '' 43 | desiredPropMap.borderRadius = '' 44 | desiredPropMap.padding = '' 45 | desiredPropMap.margin = '' 46 | desiredPropMap.gap = '' 47 | desiredPropMap.gridArea = '' 48 | desiredPropMap.borderColor = '' 49 | } 50 | 51 | if (isSafari) { 52 | desiredPropMap.gap = 'normal normal' 53 | } 54 | 55 | export const desiredAccessibilityMap = [ 56 | 'role', 57 | 'tabindex', 58 | 'aria-*', 59 | 'for', 60 | 'alt', 61 | 'title', 62 | 'type', 63 | ] 64 | 65 | export const largeWCAG2TextMap = [ 66 | { 67 | fontSize: '24px', 68 | fontWeight: '0' 69 | }, 70 | { 71 | fontSize: '18.5px', 72 | fontWeight: '700' 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /app/utilities/index.js: -------------------------------------------------------------------------------- 1 | export * from './styles' 2 | export * from './accessibility' 3 | export * from './common' 4 | export * from './strings' 5 | export * from './window' 6 | export * from './cross-browser' 7 | export * from './isFixed' 8 | export * from './scheme' 9 | export * from './colors' 10 | export * from './numbers' -------------------------------------------------------------------------------- /app/utilities/isFixed.js: -------------------------------------------------------------------------------- 1 | export const isFixed = elem => { 2 | do { 3 | if (window.getComputedStyle(elem).position == 'fixed') return true; 4 | } while (elem = elem.offsetParent); 5 | return false; 6 | } 7 | -------------------------------------------------------------------------------- /app/utilities/numbers.js: -------------------------------------------------------------------------------- 1 | export function numberBetween(min, max) { 2 | return Math.floor(min + (Math.random() * (max - min))) 3 | } 4 | 5 | export function clamp(min, val, max) { 6 | return Math.max(min, Math.min(val, max)) 7 | } 8 | -------------------------------------------------------------------------------- /app/utilities/scheme.js: -------------------------------------------------------------------------------- 1 | import { LightTheme, DarkTheme } from "../components/styles.store" 2 | 3 | export const schemeRule = (shadow, style, light, dark) => { 4 | const lightScheme = light 5 | ? [style, LightTheme, light] 6 | : [style, LightTheme] 7 | 8 | const darkScheme = dark 9 | ? [style, DarkTheme, dark] 10 | : [style, DarkTheme] 11 | 12 | return attr => { 13 | if (attr === "light") 14 | shadow.adoptedStyleSheets = lightScheme 15 | else if (attr === "dark") 16 | shadow.adoptedStyleSheets = darkScheme 17 | else 18 | shadow.adoptedStyleSheets = [style] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/utilities/strings.js: -------------------------------------------------------------------------------- 1 | export const camelToDash = (camelString = "") => 2 | camelString.replace(/([A-Z])/g, ($1) => 3 | "-"+$1.toLowerCase()) 4 | 5 | export const nodeKey = node => { 6 | let tree = [] 7 | let furthest_leaf = node 8 | 9 | while (furthest_leaf) { 10 | tree.push(furthest_leaf) 11 | furthest_leaf = furthest_leaf.parentNode 12 | ? furthest_leaf.parentNode 13 | : false 14 | } 15 | 16 | return tree.reduce((path, branch) => ` 17 | ${path}${branch.tagName}_${branch.className}_${[...node.parentNode.children].indexOf(node)}_${node.children.length} 18 | `, '') 19 | } 20 | 21 | export const createClassname = (el, ellipse = false) => { 22 | if (!el.className) return '' 23 | 24 | const combined = Array.from(el.classList).reduce((classnames, classname) => 25 | classnames += '.' + escapeSpecialCharacters(classname) 26 | , '') 27 | 28 | return ellipse && combined.length > 30 29 | ? combined.substring(0,30) + '...' 30 | : combined 31 | } 32 | 33 | const escapeSpecialCharacters = (query) => 34 | Array.from(query) 35 | .map((char) => /[0-9a-zA-Z_\s-]/.test(char) ? char : `\\${char}`) 36 | .join("") 37 | 38 | 39 | export const metaKey = window.navigator.platform.includes('Mac') 40 | ? 'cmd' 41 | : 'ctrl' 42 | 43 | export const altKey = window.navigator.platform.includes('Mac') 44 | ? 'opt' 45 | : 'alt' 46 | 47 | export const notList = ':not(vis-bug):not(script):not(hotkey-map):not(.visbug-metatip):not(visbug-label):not(visbug-handles):not(visbug-corners):not(visbug-grip):not(visbug-gridlines)' 48 | -------------------------------------------------------------------------------- /app/utilities/window.js: -------------------------------------------------------------------------------- 1 | export function windowBounds() { 2 | const height = window.innerHeight 3 | const width = window.innerWidth 4 | const body = document.documentElement.clientWidth 5 | 6 | const calcWidth = body <= width 7 | ? body 8 | : width 9 | 10 | return { 11 | winHeight: height, 12 | winWidth: calcWidth, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/tuts_src/a11y.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/a11y.gif -------------------------------------------------------------------------------- /assets/tuts_src/edittext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/edittext.gif -------------------------------------------------------------------------------- /assets/tuts_src/flexbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/flexbox.gif -------------------------------------------------------------------------------- /assets/tuts_src/guides.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/guides.gif -------------------------------------------------------------------------------- /assets/tuts_src/hueshift.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/hueshift.gif -------------------------------------------------------------------------------- /assets/tuts_src/margin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/margin.gif -------------------------------------------------------------------------------- /assets/tuts_src/metatip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/metatip.gif -------------------------------------------------------------------------------- /assets/tuts_src/move.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/move.gif -------------------------------------------------------------------------------- /assets/tuts_src/padding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/padding.gif -------------------------------------------------------------------------------- /assets/tuts_src/position.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/position.gif -------------------------------------------------------------------------------- /assets/tuts_src/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/search.gif -------------------------------------------------------------------------------- /assets/tuts_src/shadow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/shadow.gif -------------------------------------------------------------------------------- /assets/tuts_src/typography.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/tuts_src/typography.gif -------------------------------------------------------------------------------- /assets/visbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/visbug.png -------------------------------------------------------------------------------- /assets/visbug.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/assets/visbug.sketch -------------------------------------------------------------------------------- /assets/visbug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VisBug Copy 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /extension/build/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/extension/build/.keep -------------------------------------------------------------------------------- /extension/contextmenu/colormode.js: -------------------------------------------------------------------------------- 1 | const storagekey = 'visbug-color-mode' 2 | const defaultcolormode = 'hex' 3 | 4 | const color_options = [ 5 | 'hsl', 6 | 'hex', 7 | 'rgb', 8 | // 'hsv', 9 | // 'lch', 10 | // 'lab', 11 | // 'hcl', 12 | // 'cmyk', 13 | // 'gl', 14 | // 'as authored', 15 | ] 16 | 17 | const colormodestate = { 18 | mode: defaultcolormode 19 | } 20 | 21 | var platform = typeof browser === 'undefined' 22 | ? chrome 23 | : browser 24 | 25 | const sendColorMode = () => { 26 | platform.tabs.query({active: true, currentWindow: true}, ([tab]) => { 27 | tab && platform.tabs.sendMessage(tab.id, { 28 | action: 'COLOR_MODE', 29 | params: {mode:colormodestate.mode}, 30 | }) 31 | }) 32 | } 33 | 34 | export const getColorMode = () => { 35 | platform.storage.sync.get([storagekey], value => { 36 | let found_value = value[storagekey] 37 | 38 | const is_default = found_value 39 | ? value[storagekey] === defaultcolormode 40 | : false 41 | 42 | // first run 43 | if (!found_value && !is_default) { 44 | found_value = defaultcolormode 45 | platform.storage.sync.set({[storagekey]: defaultcolormode}) 46 | } 47 | 48 | // migrate old choices 49 | if (found_value === 'hsla') { 50 | found_value = 'hsl' 51 | platform.storage.sync.set({[storagekey]: found_value}) 52 | } 53 | if (found_value === 'rgba') { 54 | found_value = 'rgb' 55 | platform.storage.sync.set({[storagekey]: found_value}) 56 | } 57 | 58 | // update checked state of color contextmenu radio list 59 | color_options.forEach(option => { 60 | platform.contextMenus.update(option, { 61 | checked: option === found_value 62 | }) 63 | }) 64 | 65 | // send visbug user preference 66 | colormodestate.mode = found_value 67 | sendColorMode() 68 | 69 | return found_value 70 | }) 71 | } 72 | 73 | // load synced color choice on load 74 | getColorMode() 75 | 76 | platform.contextMenus.create({ 77 | id: 'color-mode', 78 | title: 'Colors', 79 | contexts: ['all'], 80 | }) 81 | 82 | color_options.forEach(option => { 83 | platform.contextMenus.create({ 84 | id: option, 85 | parentId: 'color-mode', 86 | title: ' '+option, 87 | checked: false, 88 | type: 'radio', 89 | contexts: ['all'], 90 | }) 91 | }) 92 | 93 | platform.contextMenus.onClicked.addListener(({parentMenuItemId, menuItemId}, tab) => { 94 | if (parentMenuItemId !== 'color-mode') return 95 | 96 | platform.storage.sync.set({[storagekey]: menuItemId}) 97 | colormodestate.mode = menuItemId 98 | 99 | sendColorMode() 100 | }) 101 | -------------------------------------------------------------------------------- /extension/contextmenu/colorscheme.js: -------------------------------------------------------------------------------- 1 | const schemestoragekey = 'visbug-color-scheme'; 2 | const defaultcolorscheme = 'auto'; 3 | 4 | const scheme_option = [ 5 | 'auto', 6 | 'light', 7 | 'dark', 8 | ] 9 | 10 | const colorschemestate = { 11 | mode: defaultcolorscheme 12 | } 13 | 14 | var platform = typeof browser === 'undefined' 15 | ? chrome 16 | : browser 17 | 18 | const sendColorScheme = () => { 19 | platform.tabs.query({active: true, currentWindow: true}, ([tab]) => { 20 | tab && platform.tabs.sendMessage(tab.id, { 21 | action: 'COLOR_SCHEME', 22 | params: {mode:colorschemestate.mode}, 23 | }) 24 | }) 25 | } 26 | 27 | export const getColorScheme = () => { 28 | platform.storage.sync.get([schemestoragekey], value => { 29 | let found_value = value[schemestoragekey]; 30 | 31 | // first run 32 | if (!found_value) { 33 | found_value = defaultcolorscheme; 34 | platform.storage.sync.set({ [schemestoragekey]: defaultcolorscheme }); 35 | } 36 | 37 | // update checked state of scheme contextmenu radio list 38 | scheme_option.forEach(option => { 39 | platform.contextMenus.update(option, { 40 | checked: option === found_value 41 | }) 42 | }) 43 | 44 | // send visbug user preference 45 | colorschemestate.mode = found_value 46 | sendColorScheme() 47 | 48 | return found_value 49 | }) 50 | } 51 | 52 | // load synced scheme choice on load 53 | getColorScheme() 54 | 55 | platform.contextMenus.create({ 56 | id: 'color-scheme', 57 | title: 'Theme', 58 | contexts: ['all'], 59 | }) 60 | 61 | scheme_option.forEach(option => { 62 | platform.contextMenus.create({ 63 | id: option, 64 | parentId: 'color-scheme', 65 | title: ' '+option, 66 | checked: false, 67 | type: 'radio', 68 | contexts: ['all'], 69 | }) 70 | }) 71 | 72 | platform.contextMenus.onClicked.addListener(({parentMenuItemId, menuItemId}, tab) => { 73 | if (parentMenuItemId !== 'color-scheme') return 74 | 75 | platform.storage.sync.set({[schemestoragekey]: menuItemId}) 76 | colorschemestate.mode = menuItemId 77 | 78 | sendColorScheme() 79 | }) 80 | -------------------------------------------------------------------------------- /extension/contextmenu/launcher.js: -------------------------------------------------------------------------------- 1 | var platform = typeof browser === 'undefined' 2 | ? chrome 3 | : browser 4 | 5 | var toggleIt 6 | 7 | export const gimmeToggle = toggleIn => { 8 | toggleIt = toggleIn 9 | platform.action.onClicked.addListener(toggleIt) 10 | } 11 | 12 | platform.contextMenus.create({ 13 | id: 'launcher', 14 | title: 'Show/Hide', 15 | contexts: ['all'], 16 | }) 17 | 18 | platform.contextMenus.onClicked.addListener(({menuItemId}, tab) => { 19 | if (menuItemId === 'launcher') 20 | toggleIt(tab) 21 | }) 22 | -------------------------------------------------------------------------------- /extension/icons/visbug-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/extension/icons/visbug-dev.png -------------------------------------------------------------------------------- /extension/icons/visbug-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/extension/icons/visbug-original.png -------------------------------------------------------------------------------- /extension/icons/visbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/ProjectVisBug/382ecff58dec46b5e1c21e9d1801c4a58afda599/extension/icons/visbug.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DevBug", 3 | "version": "0.4.9", 4 | "description": "Open source browser design tools", 5 | "manifest_version": 3, 6 | "icons": { "128": "icons/visbug-dev.png" }, 7 | "permissions": [ 8 | "activeTab", 9 | "contextMenus", 10 | "scripting", 11 | "storage" 12 | ], 13 | "background": { 14 | "service_worker": "visbug.js", 15 | "type": "module" 16 | }, 17 | "action": { 18 | "default_title": "Click or press Alt+Shift+D to launch VisBug", 19 | "default_icon": { 20 | "128": "icons/visbug.png" 21 | } 22 | }, 23 | "web_accessible_resources": [{ 24 | "resources": [ 25 | "tuts/*.gif", 26 | "toolbar/*" 27 | ], 28 | "matches": [""] 29 | }], 30 | "commands": { 31 | "_execute_action": { 32 | "suggested_key": { 33 | "windows": "Alt+Shift+D", 34 | "mac": "Alt+Shift+D", 35 | "chromeos": "Alt+Shift+D", 36 | "linux": "Alt+Shift+D" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/toolbar/eject.js: -------------------------------------------------------------------------------- 1 | document.querySelectorAll('vis-bug') 2 | .forEach(node => { 3 | node.animate( 4 | [{transform: 'translateX(-200%)', opacity:0}], 5 | { 6 | duration: 300, 7 | easing: 'ease-out', 8 | }).onfinish = e => node.remove() 9 | }) 10 | -------------------------------------------------------------------------------- /extension/toolbar/inject.js: -------------------------------------------------------------------------------- 1 | var platform = typeof browser === 'undefined' 2 | ? chrome 3 | : browser 4 | 5 | const script = document.createElement('script') 6 | script.type = 'module' 7 | script.src = platform.runtime.getURL('toolbar/bundle.min.js') 8 | document.body.appendChild(script) 9 | 10 | const visbug = document.createElement('vis-bug') 11 | 12 | const src_path = platform.runtime.getURL(`tuts/guides.gif`) 13 | visbug.setAttribute('tutsBaseURL', src_path.slice(0, src_path.lastIndexOf('/'))) 14 | 15 | document.body.prepend(visbug) 16 | 17 | platform.runtime.onMessage.addListener(request => { 18 | if (request.action === 'COLOR_MODE') 19 | visbug.setAttribute('color-mode', request.params.mode) 20 | else if (request.action === 'COLOR_SCHEME') 21 | visbug.setAttribute("color-scheme", request.params.mode) 22 | }) 23 | -------------------------------------------------------------------------------- /extension/toolbar/restore.js: -------------------------------------------------------------------------------- 1 | var platform = typeof browser === 'undefined' 2 | ? chrome 3 | : browser 4 | 5 | var restore = () => { 6 | const visbug = document.createElement('vis-bug') 7 | const src_path = platform.runtime.getURL(`tuts/guides.gif`) 8 | 9 | visbug.setAttribute('tutsBaseURL', src_path.slice(0, src_path.lastIndexOf('/'))) 10 | document.body.prepend(visbug) 11 | } 12 | 13 | restore() 14 | -------------------------------------------------------------------------------- /extension/visbug.js: -------------------------------------------------------------------------------- 1 | import {gimmeToggle} from "./contextmenu/launcher.js" 2 | import {getColorMode} from "./contextmenu/colormode.js" 3 | import {getColorScheme} from "./contextmenu/colorscheme.js" 4 | 5 | const state = { 6 | loaded: {}, 7 | injected: {}, 8 | } 9 | 10 | var platform = typeof browser === 'undefined' 11 | ? chrome 12 | : browser 13 | 14 | const toggleIn = ({id:tab_id}) => { 15 | // toggle out: it's currently loaded and injected 16 | if (state.loaded[tab_id] && state.injected[tab_id]) { 17 | platform.scripting.executeScript({ 18 | target: {tabId: tab_id}, 19 | files: ['toolbar/eject.js'], 20 | }) 21 | state.injected[tab_id] = false 22 | } 23 | 24 | // toggle in: it's loaded and needs injected 25 | else if (state.loaded[tab_id] && !state.injected[tab_id]) { 26 | platform.scripting.executeScript({ 27 | target: {tabId: tab_id}, 28 | files: ['toolbar/restore.js'], 29 | }) 30 | state.injected[tab_id] = true 31 | getColorMode() 32 | getColorScheme() 33 | } 34 | 35 | // fresh start in tab 36 | else { 37 | platform.scripting.insertCSS({ 38 | target: {tabId: tab_id}, 39 | files: ['toolbar/bundle.css' ], 40 | }) 41 | platform.scripting.executeScript({ 42 | target: {tabId: tab_id}, 43 | files: ['toolbar/inject.js'], 44 | }) 45 | 46 | state.loaded[tab_id] = true 47 | state.injected[tab_id] = true 48 | getColorMode() 49 | getColorScheme() 50 | } 51 | 52 | platform.tabs.onUpdated.addListener(function(tabId) { 53 | if (tabId === tab_id) 54 | state.loaded[tabId] = false 55 | }) 56 | } 57 | 58 | gimmeToggle(toggleIn) 59 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "app", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VisBug", 3 | "version": "0.4.9", 4 | "description": "", 5 | "author": "Adam Argyle", 6 | "license": "Apache-2.0", 7 | "main": "app/components/vis-bug/vis-bug.element.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/GoogleChromeLabs/ProjectVisBug.git" 11 | }, 12 | "scripts": { 13 | "start": "npm run concurrent", 14 | "concurrent": "concurrently --kill-others \"npm run dev:js\" \"npm run dev:css\" \"npm run dev:server\"", 15 | "bundle": "rollup -c && postcss app/index.css -o app/bundle.css", 16 | "bundle:prod": "rollup -c --environment build:prod", 17 | "bump": "npm version patch -m 'Release %s'", 18 | "postversion": "npm run extension:ci:version", 19 | "dev:js": "rollup -c -w --environment build:dev", 20 | "dev:css": "postcss app/index.css -o app/bundle.css -w", 21 | "dev:server": "browser-sync start --server \"app\" --files \"app/index.html,app/bundle.css,app/bundle.js\" --no-open --no-notify --no-ui --no-ghost-mode", 22 | "dev:extension": "npm run extension:build && npm run extension:local:version", 23 | "extension": "npm run extension:js && npm run extension:css && npm run extension:copy && npm run extension:local:version", 24 | "extension:local:version": "sed -i '' \"s/{{NPM_VERSION}}/0.0.0/\" ./extension/manifest.json && sed -i '' \"s/VisBug/DevBug/\" ./extension/manifest.json && sed -i '' \"s/visbug.png/visbug-dev.png/\" ./extension/manifest.json", 25 | "extension:ci:version": "sed -i \"s/{{NPM_VERSION}}/$npm_package_version/\" ./extension/manifest.json", 26 | "extension:build": "npm run extension:js && npm run extension:css && npm run extension:copy", 27 | "extension:release": "npm run test:ci && npm run bump && npm run extension:build && npm run extension:zip", 28 | "extension:js": "npm run bundle:prod", 29 | "extension:css": "postcss app/extension.css -o extension/toolbar/bundle.css", 30 | "extension:copy": "cp app/bundle.min.js extension/toolbar/ && cp -R app/tuts/ extension/tuts", 31 | "extension:zip": "rm -rf ./extension/build/* && mkdir ./visbug_v$npm_package_version && cp -R ./extension/* ./visbug_v$npm_package_version/ && zip -r ./extension/build/visbug.zip ./visbug_v$npm_package_version/ && rm -rf ./visbug_v$npm_package_version", 32 | "extension:firefox": "npm run dev:extension && cd extension && web-ext run", 33 | "extension:firefox-build": "npm run extension:build && cd extension && web-ext build", 34 | "deploy": "gcloud app deploy --project=visbug-1337", 35 | "test": "ava", 36 | "test:dev": "ava -v -w", 37 | "test:server": "browser-sync start --server \"app\" --files \"app/index.html,app/bundle.css,app/bundle.js\" --no-open --no-notify --no-ui --no-ghost-mode", 38 | "test:ci": "start-server-and-test http://localhost:3000" 39 | }, 40 | "dependencies": { 41 | "@ctrl/tinycolor": "^3.0.2", 42 | "blingblingjs": "^2.3.0", 43 | "colorjs.io": "^0.5.0", 44 | "construct-style-sheets-polyfill": "^2.4.2", 45 | "hotkeys-js": "^3.13.7", 46 | "query-selector-shadow-dom": "^1.0.1" 47 | }, 48 | "devDependencies": { 49 | "ava": "1.4.1", 50 | "browser-sync": "^2.26.13", 51 | "concurrently": "^5.1.0", 52 | "esm": "^3.2.22", 53 | "open-props": "^1.6.21", 54 | "postcss": "^7.0.27", 55 | "postcss-cli": "^7.1.0", 56 | "postcss-import": "^12.0.1", 57 | "postcss-loader": "^3.0.0", 58 | "postcss-preset-env": "^6.7.0", 59 | "puppeteer": "^10.0.0", 60 | "ragrid": "^1.0.6", 61 | "rollup": "^2.0.0", 62 | "rollup-plugin-node-resolve": "^5.2.0", 63 | "rollup-plugin-postcss": "^3.1.0", 64 | "rollup-plugin-terser": "^7.0.2", 65 | "start-server-and-test": "^1.11.0", 66 | "web-ext": "^7.11.0" 67 | }, 68 | "ava": { 69 | "require": [ 70 | "esm" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env') 2 | const postcssImport = require('postcss-import') 3 | 4 | module.exports = { 5 | plugins: [ 6 | postcssImport(), 7 | postcssPresetEnv({ 8 | stage: 0, 9 | browsers: [ 10 | 'last 3 chrome version', 11 | 'last 3 firefox version', 12 | ], 13 | }), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import postcss from 'rollup-plugin-postcss' 3 | import {terser} from 'rollup-plugin-terser' 4 | 5 | const is_prod = process.env.build === 'prod' 6 | 7 | const dev_plugins = [ 8 | resolve({ 9 | jsnext: true, 10 | }), 11 | postcss({ 12 | extract: false, 13 | inject: false, 14 | }), 15 | ] 16 | 17 | const prod_plugins = [ 18 | terser(), 19 | ] 20 | 21 | const plugins = is_prod 22 | ? [...dev_plugins, ...prod_plugins] 23 | : dev_plugins 24 | 25 | export default { 26 | input: 'app/index.js', 27 | output: { 28 | file: is_prod ? 'app/bundle.min.js' : 'app/bundle.js', 29 | format: 'es', 30 | sourcemap: is_prod ? null : 'inline', 31 | }, 32 | plugins, 33 | watch: { 34 | exclude: ['node_modules/**'], 35 | } 36 | } -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | 3 | export const setupPptrTab = async t => { 4 | t.context.browser = await puppeteer.launch({ 5 | // headless: false, 6 | args: ['--no-sandbox'] 7 | }) 8 | t.context.page = await t.context.browser.newPage() 9 | 10 | await t.context.page.goto('http://localhost:3000') 11 | await t.context.page.evaluateHandle(`document.body.setAttribute('testing', true)`) 12 | await t.context.page.waitForSelector('vis-bug') 13 | } 14 | 15 | export const teardownPptrTab = async ({context:{ page, browser }}) => { 16 | await page.close() 17 | } 18 | 19 | export const changeMode = async ({page, tool}) => 20 | await page.evaluateHandle(` 21 | var mouseUpEvent = document.createEvent("MouseEvents"); 22 | mouseUpEvent.initEvent("mouseup", true, true); 23 | document.querySelector('vis-bug').$shadow.querySelector('li[data-tool=${tool}]').dispatchEvent(mouseUpEvent); 24 | `) 25 | 26 | export const getActiveTool = async page => 27 | await page.$eval('vis-bug', el => 28 | el.activeTool) 29 | 30 | export const pptrMetaKey = async page => { 31 | let isMac = await page.evaluate(_ => window.navigator.platform.includes('Mac')) 32 | return isMac ? "Meta" : "Control" 33 | } --------------------------------------------------------------------------------