├── .bowerrc ├── .travis.yml ├── images ├── icon.png ├── icon128.png ├── spinner.png ├── add-icon.png ├── command-default.png └── icon-highlight.png ├── fonts ├── Lato-Bold.ttf ├── Lato-Italic.ttf ├── Lato-Light.ttf └── Lato-Regular.ttf ├── scripts ├── test.js ├── config.js ├── lib │ ├── analytics.js │ ├── add-gist.js │ ├── store.js │ ├── console.js │ └── commands.js ├── main.js ├── content-script.js ├── background.js └── options.js ├── background.html ├── test.html ├── styles ├── _animations.scss ├── style.scss └── style.css ├── LICENSE ├── package.json ├── .gitignore ├── README.md ├── manifest.json ├── options.html └── Gruntfile.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/vendor/" 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/icon128.png -------------------------------------------------------------------------------- /images/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/spinner.png -------------------------------------------------------------------------------- /fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /fonts/Lato-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/fonts/Lato-Italic.ttf -------------------------------------------------------------------------------- /fonts/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/fonts/Lato-Light.ttf -------------------------------------------------------------------------------- /images/add-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/add-icon.png -------------------------------------------------------------------------------- /fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /images/command-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/command-default.png -------------------------------------------------------------------------------- /images/icon-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backtickbacktick/backtick/HEAD/images/icon-highlight.png -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | chrome.storage.sync.get(null, storage => { 2 | $('pre').html(JSON.stringify(storage, null, '\t')); 3 | }); -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backtick 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | githubApiUrl: 'https://api.github.com', 3 | extensionID: 'daiejhinmmfgincamkeeobmpffhdljim', 4 | defaultHotkey: '`', 5 | defaultCommandIcon: 'https://backtick.ninja/assets/images/command-default.png', 6 | libraryCommandsJson: 'https://backtick.ninja/library/commands.json' 7 | }; -------------------------------------------------------------------------------- /scripts/lib/analytics.js: -------------------------------------------------------------------------------- 1 | function GoogleAnalytics() { 2 | (function(i, s, o, g, r, a, m) { 3 | i['GoogleAnalyticsObject'] = r; 4 | i[r] = i[r] || function() { 5 | (i[r].q = i[r].q || []).push(arguments); 6 | }, i[r].l = 1 * new Date(); 7 | a = s.createElement(o), 8 | m = s.getElementsByTagName(o)[0]; 9 | a.async = 1; 10 | a.src = g; 11 | m.parentNode.insertBefore(a, m); 12 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); 13 | ga('create', 'UA-51020929-7', 'auto'); 14 | ga('send', 'pageview'); 15 | } -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backtick Test 6 | 21 | 22 | 23 |

24 | 
25 | 
26 | 
27 | 


--------------------------------------------------------------------------------
/styles/_animations.scss:
--------------------------------------------------------------------------------
 1 | @-webkit-keyframes _bt-console-in {
 2 |   0% {
 3 |     opacity: 0;
 4 |     -webkit-transform: translate3D(0, -100px, 0);
 5 |   }
 6 |   100% {
 7 |     opacity: 1;
 8 |   }
 9 | }
10 | 
11 | @-webkit-keyframes _bt-console-out {
12 |   0% {
13 |     opacity: 1;
14 |   }
15 |   100% {
16 |     opacity: 0;
17 |     -webkit-transform: translate3D(0, -100px, 0);
18 |   }
19 | }
20 | 
21 | @-webkit-keyframes _bt-spinner {
22 |   100% {
23 |     background-position-x: -100%;
24 |   }
25 | }
26 | 
27 | @-webkit-keyframes _bt-fade-in {
28 |   0% {
29 |     opacity: 0;
30 |   }
31 | }
32 | 
33 | @-webkit-keyframes _bt-fade-out {
34 |   100% {
35 |     opacity: 0;
36 |   }
37 | }


--------------------------------------------------------------------------------
/scripts/main.js:
--------------------------------------------------------------------------------
 1 | window.slugify = function(text) {
 2 |     return text.toString().toLowerCase()
 3 |         .replace(/\s+/g, '-')           // Replace spaces with -
 4 |         .replace(/[^\w\-]+/g, '')       // Remove all non-word and non - chars
 5 |         .replace(/\-\-+/g, '-')         // Replace multiple - with single -
 6 |         .replace(/^-+/, '')             // Trim - from start of text
 7 |         .replace(/-+$/, '');            // Trim - from end of text
 8 | };
 9 | 
10 | new BacktickStore().then(backtickStore => {
11 | 
12 |     let AddGist = BacktickAddGistInitiator(backtickStore);
13 |     let backtickCommands = new BacktickCommands(backtickStore);
14 |     new BacktickConsole(backtickStore, backtickCommands, AddGist);
15 | 
16 | });


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2017 Backtick
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "backtick",
 3 |   "version": "0.6.3",
 4 |   "description": "The Rebuilt Backtick Chrome Extension",
 5 |   "main": "index.js",
 6 |   "scripts": {
 7 |     "test": "echo \"No test specified\" && exit 0"
 8 |   },
 9 |   "repository": {
10 |     "type": "git",
11 |     "url": "git+https://iambriansreed@github.com/backtickbacktick/Backtick2.git"
12 |   },
13 |   "author": "Brian Reed  (http://iambrian.com)",
14 |   "license": "MIT",
15 |   "bugs": {
16 |     "url": "https://github.com/backtickbacktick/Backtick2/issues"
17 |   },
18 |   "homepage": "https://github.com/backtickbacktick/Backtick2#readme",
19 |   "devDependencies": {
20 |     "babel-plugin-transform-class-properties": "^6.24.1",
21 |     "babel-preset-es2015": "^6.24.1",
22 |     "grunt": "^1.0.1",
23 |     "grunt-babel": "^6.0.0",
24 |     "grunt-contrib-clean": "^1.1.0",
25 |     "grunt-contrib-concat": "^1.0.1",
26 |     "grunt-contrib-copy": "^1.0.0",
27 |     "grunt-contrib-uglify": "^3.0.1",
28 |     "grunt-contrib-watch": "^1.0.0",
29 |     "grunt-sass": "^2.0.0",
30 |     "load-grunt-tasks": "^3.5.2"
31 |   },
32 |   "dependencies": {
33 |     "jquery": "^3.2.1"
34 |   }
35 | }
36 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | 
 8 | # Runtime data
 9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 | 
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 | 
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 | 
20 | # nyc test coverage
21 | .nyc_output
22 | 
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 | 
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 | 
29 | # node-waf configuration
30 | .lock-wscript
31 | 
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 | 
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 | 
39 | # Typescript v1 declaration files
40 | typings/
41 | 
42 | # Optional npm cache directory
43 | .npm
44 | 
45 | # Optional eslint cache
46 | .eslintcache
47 | 
48 | # Optional REPL history
49 | .node_repl_history
50 | 
51 | # Output of 'npm pack'
52 | *.tgz
53 | 
54 | # Yarn Integrity file
55 | .yarn-integrity
56 | 
57 | # dotenv environment variables file
58 | .env
59 | 
60 | .tmp
61 | _dist
62 | Archive.zip
63 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Backtick
 2 | 
 3 | **Backtick is a command line for bookmarklets and scripts**, packaged as a Chrome extension. ~~For a better explanation, try out the demo at [backtick.ninja/](http://backtick.ninja)~~ Coming soon. The extension is free to use.
 4 | 
 5 | *[MIT Licensed](http://opensource.org/licenses/MIT) 2017 Brian Reed*
 6 | 
 7 | #### Developing
 8 | The code is open for you to play around with and contribute to, if you wish. Here's some instructions on how to get up and running.
 9 | 
10 | 
11 | ##### Dependencies
12 | To compile Backtick, you'll need to install the following dependencies on your system:
13 |   * [Node.js](http://nodejs.org/)
14 |   * [Grunt](http://gruntjs.com/)
15 | 
16 | With that installed, run these two commands to download the NPM and Bower packages:
17 | ```bash
18 | $ npm install
19 | ```
20 | 
21 | ##### Building
22 | To build all the files, just run `grunt` in the root folder. This will put all built files into the *_dist/* folder.
23 | 
24 | To load the built extension files from the *_dist/* folder into Chrome, [follow these instructions](http://developer.chrome.com/extensions/getstarted.html#unpacked).
25 | 
26 | ##### Updates
27 | This new version of Backtick is still under development, check for updates on [twitter](https://twitter.com/backtickninja). 
28 | 


--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"manifest_version": 2,
 3 | 	"name": "Backtick",
 4 | 	"short_name": "Backtick",
 5 | 	"description": "Powerful commands in your browser.",
 6 | 	"version": "0.6.3",
 7 | 	"icons": {
 8 | 		"16": "images/icon128.png",
 9 | 		"32": "images/icon128.png",
10 | 		"48": "images/icon128.png",
11 | 		"128": "images/icon128.png"
12 | 	},
13 | 	"background": {
14 | 		"page": "background.html"
15 | 	},
16 | 	"options_page": "options.html",
17 | 	"browser_action": {
18 | 		"default_icon": {
19 | 			"19": "images/icon128.png",
20 | 			"38": "images/icon128.png"
21 | 		},
22 | 		"default_title": "Open the Backtick console"
23 | 	},
24 | 	"content_scripts": [
25 | 		{
26 | 			"matches": [
27 | 				""
28 | 			],
29 | 			"js": [
30 | 				"scripts/content-script.js"
31 | 			]
32 | 		}
33 | 	],
34 | 	"content_security_policy": "script-src 'self' https://www.google-analytics.com; object-src 'self'",
35 | 	"permissions": [
36 | 		"tabs",
37 | 		"storage",
38 | 		"unlimitedStorage",
39 | 		""
40 | 	],
41 | 	"web_accessible_resources": [
42 | 		"images/*.png",
43 | 		"styles/style.css",
44 | 		"fonts/Lato-Bold.ttf",
45 | 		"fonts/Lato-Italic.ttf",
46 | 		"fonts/Lato-Light.ttf",
47 | 		"fonts/Lato-Regular.ttf"
48 | 	],
49 | 	"externally_connectable": {
50 | 		"matches": [
51 | 			"*://*.github.com/*"
52 | 		]
53 | 	}
54 | }
55 | 


--------------------------------------------------------------------------------
/scripts/content-script.js:
--------------------------------------------------------------------------------
 1 | (function() {
 2 | 
 3 |     chrome.storage.sync.get(null, storage => {
 4 | 
 5 |         if (storage.message) {
 6 |             alert(storage.message);
 7 |             loadExtension();
 8 |         }
 9 | 
10 |         let hotkey = storage.hotkey || '`';
11 |         document.addEventListener('keypress', event => onKeypress(event, hotkey), true);
12 |     });
13 | 
14 |     function onKeypress(event, hotkey) {
15 | 
16 |         // not hotkey or is an editable field
17 |         const key = String.fromCharCode(event.which);
18 |         const nodeName = document.activeElement.nodeName.toLowerCase();
19 | 
20 |         if (hotkey !== key
21 |             || document.activeElement.isContentEditable
22 |             || 'input' === nodeName || 'textarea' === nodeName || 'select' === nodeName) {
23 |             return;
24 |         }
25 | 
26 |         loadExtension();
27 | 
28 |         return false;
29 |     }
30 | 
31 |     function loadExtension() {
32 | 
33 |         // let the initial load event clear out
34 |         setTimeout(() => {
35 |             chrome.runtime.sendMessage({ action: 'LoadBacktickPlease' }, function(response) {
36 |                 // loaded successfully ? stop listening
37 |                 if (response === 'Loaded') {
38 |                     document.removeEventListener('keypress', onKeypress, true);
39 |                 }
40 |             });
41 |         }, 100);
42 |     }
43 | })();


--------------------------------------------------------------------------------
/scripts/background.js:
--------------------------------------------------------------------------------
 1 | /* globals $ */
 2 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
 3 | 
 4 |     if (!request.action) {
 5 |         return;
 6 |     }
 7 | 
 8 |     if (request.action === 'LoadBacktickPlease') {
 9 |         chrome.tabs.insertCSS(sender.tab.id, { file: 'styles/style.css' });
10 |         chrome.tabs.executeScript(sender.tab.id, { file: 'scripts/jquery.min.js' });
11 |         chrome.tabs.executeScript(sender.tab.id, { file: 'scripts/main.js' });
12 |         sendResponse('Loaded');
13 |     }
14 | 
15 |     if (request.action === 'LoadBacktickCommand' && request.script) {
16 | 
17 |         const JAVASCRIPT_URL_REGEXP = /^javascript:/;
18 | 
19 |         function convertSourceToUrl(url) {
20 | 
21 |             if (JAVASCRIPT_URL_REGEXP.test(url)) {
22 |                 url = url.replace(JAVASCRIPT_URL_REGEXP, '');
23 |                 try { url = decodeURIComponent(url); }
24 |                 catch (e) {}
25 |             }
26 |             return `javascript:${url}`;
27 |         }
28 | 
29 |         return $.ajax(request.script)
30 |             .done(response => {
31 |                 let url = convertSourceToUrl(response);
32 |                 chrome.tabs.update(sender.tab.id, { url });
33 |                 sendResponse('Command run.');
34 |             })
35 |             .fail(error => {
36 |                 sendResponse(error);
37 |                 alert(error);
38 |             });
39 |     }
40 | 
41 |     if (request.action === 'LoadBacktickChromeTabs' && request.tabAction) {
42 |         chrome.tabs[request.tabAction](request.params || {});
43 |     }
44 | 
45 | });
46 | 


--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     
 5 |     Backtick Settings
 6 |     
 7 | 
 8 | 
 9 | 
10 |

Backtick Settings

11 | 12 |
13 | 14 | 15 |

16 | Note: You can also set a hotkey through the Chrome 18 | settings page. Do this if other extensions are interfering with the Backtick 19 | hotkey, 20 | or vice versa. 21 | 22 |

23 |
24 | 25 |
26 |

Your Commands

27 |

28 | Commands you or someone else create that aren't officially supported. 29 |

30 |
31 | 32 | 33 |
34 |
    35 |
  • No commands selected.
  • 36 |
37 | Learn 38 | how to create and add custom commands. 39 |
40 | 41 | 42 |
43 |

Curated Commands

44 |

45 | Curated Commands created by the Backtick team or contributed and reviewed. These 46 | commands are considered safe and are officially supported. 47 | 48 |

49 |
    50 |
  • All library commands selected.
  • 51 |
52 |
53 | 54 |
55 | 56 | The rebuilt Backtick was made by @iambriansreed, and is open sourced on 60 | GitHub. The original version of Backtick was developed by @JoelBesada. 62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /scripts/lib/add-gist.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | function BacktickAddGistInitiator(store) { 3 | 4 | return (gistId) => { 5 | 6 | return (!gistId ? getGistIdFromUrl() : Promise.resolve(gistId)) 7 | .then(fetchGist) 8 | .then(commandFromGist) 9 | .then(newCommand => { 10 | if (store.getCommands().filter(command => newCommand.slug === command.slug).length) { 11 | return Promise.reject('Already saved this command.'); 12 | } 13 | return store.addCommands([newCommand]) 14 | .then(() => newCommand); 15 | }); 16 | 17 | function fetchGist(gistId) { 18 | return new Promise((resolve, reject) => { 19 | return $.getJSON(`${config.githubApiUrl}/gists/${gistId}?t=${Date.now()}`) 20 | .fail(() => { 21 | return reject(`Unable to get gist with id '${gistId}'.`); 22 | }) 23 | .done(gist => { 24 | return resolve(gist); 25 | }); 26 | }); 27 | } 28 | 29 | function parseJson(json) { 30 | try { 31 | return JSON.parse(json); 32 | } catch (e) { 33 | return { error: 'The command.json file in the gist is not a valid JSON file.' }; 34 | } 35 | } 36 | 37 | function commandFromGist(gist) { 38 | 39 | let command = { command: null }; 40 | 41 | if (gist.hasOwnProperty('files') 42 | && gist.files.hasOwnProperty('command.js') 43 | && gist.files['command.js'].hasOwnProperty('raw_url')) { 44 | command.command = gist.files['command.js']['raw_url']; 45 | } else { 46 | return Promise.reject('Gist is missing the command.js file.'); 47 | } 48 | 49 | if (gist.hasOwnProperty('files') 50 | && gist.files.hasOwnProperty('command.json') 51 | && gist.files['command.json'].hasOwnProperty('content')) { 52 | command = Object.assign({}, parseJson(gist.files['command.json']['content']), command); 53 | if (command.error) { 54 | return Promise.reject(command.error); 55 | } 56 | } else { 57 | return Promise.reject('Gist is missing the command.json file.'); 58 | } 59 | 60 | if (command.icon && command.icon.indexOf('https://') !== 0) { 61 | return Promise.reject('Command icon url must start with "https://".'); 62 | } 63 | else if (!command.name) { 64 | return Promise.reject('Command name missing.'); 65 | } 66 | else if (!command.description) { 67 | return Promise.reject('Command description missing.'); 68 | } 69 | 70 | command.slug = gist.id; 71 | 72 | return Promise.resolve(command); 73 | } 74 | 75 | function getGistIdFromUrl() { 76 | 77 | if (window.location.host !== 'gist.github.com') { 78 | return Promise.reject('You need to be on gist.github.com to add a command.'); 79 | } 80 | 81 | let path = location.pathname; 82 | return Promise.resolve(path.slice(path.lastIndexOf('/') + 1)); 83 | } 84 | }; 85 | } -------------------------------------------------------------------------------- /scripts/lib/store.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | function BacktickStore() { 3 | 4 | // initialize store 5 | let store = { commands: [], hotkey: config.defaultHotkey }, 6 | service = { 7 | addHotkey, 8 | addCommands, 9 | removeCommands, 10 | addMessage, 11 | getLibrary, 12 | getHotkey: () => ('' + store.hotkey), 13 | getCommands: () => $.extend(true, [], store.commands), 14 | }; 15 | 16 | return getStore() 17 | .then(storage => { 18 | 19 | let initActions = []; 20 | 21 | storage = storage || {}; 22 | 23 | if (storage.message) { 24 | alert(storage.message); 25 | chrome.storage.sync.set({ message: null }); 26 | } 27 | 28 | let overwrite = 0; 29 | if (!overwrite && isValidCommands(storage['commands'])) { 30 | store.commands = storage['commands']; 31 | } else { 32 | let setCommands = getLibrary().then(addCommands); 33 | initActions.push(setCommands); 34 | } 35 | 36 | if (isValidString(storage['hotkey'])) { 37 | store.hotkey = storage['hotkey']; 38 | } else { 39 | initActions.push(addHotkey(config.defaultHotkey)); 40 | } 41 | 42 | return Promise.all(initActions) 43 | .then(() => Promise.resolve(service)) 44 | .catch(error => alert); 45 | }); 46 | 47 | function getStore() { 48 | return new Promise(resolve => { 49 | chrome.storage.sync.get(null, storage => { 50 | resolve(storage); 51 | }); 52 | }); 53 | } 54 | 55 | function isValidString(hotkey) { 56 | return hotkey && 'string' === typeof(hotkey); 57 | } 58 | 59 | function isValidCommands(commands) { 60 | return commands && Array.isArray(commands); 61 | } 62 | 63 | function addHotkey(newHotkey) { 64 | 65 | return new Promise((resolve, reject) => { 66 | 67 | if (!isValidString(newHotkey)) { 68 | return reject('New hotkey is not a string.'); 69 | } 70 | 71 | store.hotkey = newHotkey; 72 | 73 | chrome.storage.sync.set({ hotkey: store.hotkey }, function() { 74 | return resolve(store.hotkey); 75 | }); 76 | 77 | }); 78 | } 79 | 80 | function addMessage(newMessage) { 81 | 82 | return new Promise((resolve, reject) => { 83 | 84 | if (!isValidString(newMessage)) { 85 | return reject('New message is not a string.'); 86 | } 87 | 88 | store.message = newMessage; 89 | 90 | chrome.storage.sync.set({ message: store.message }, function() { 91 | return resolve(store.message); 92 | }); 93 | 94 | }); 95 | } 96 | 97 | function addCommands(newCommands) { 98 | 99 | return new Promise((resolve, reject) => { 100 | 101 | if (!isValidCommands(newCommands)) { 102 | return reject('New commands are not an array.'); 103 | } 104 | 105 | store.commands = newCommands.concat(store.commands); 106 | 107 | chrome.storage.sync.set({ commands: store.commands }, function() { 108 | return resolve(store.commands); 109 | }); 110 | 111 | }); 112 | } 113 | 114 | function removeCommands(removeCommands) { 115 | 116 | return new Promise((resolve, reject) => { 117 | 118 | if (!isValidCommands(removeCommands)) { 119 | return reject('Commands are not an array.'); 120 | } 121 | 122 | let removeCommandSlugs = removeCommands.map(command => command.slug); 123 | 124 | store.commands = store.commands.filter(command => !removeCommandSlugs.includes(command.slug)); 125 | 126 | chrome.storage.sync.set({ commands: store.commands }, function() { 127 | return resolve(store.commands); 128 | }); 129 | 130 | }); 131 | } 132 | 133 | function getLibrary() { 134 | return new Promise((resolve, reject) => { 135 | $.getJSON(config.libraryCommandsJson) 136 | .fail(() => reject('Could not load library commands.')) 137 | .done(defaultCommands => { 138 | return resolve(defaultCommands); 139 | }); 140 | }); 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /scripts/lib/console.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | function BacktickConsole(store, commands, addGist) { 3 | 4 | if (!(this instanceof BacktickConsole)) { 5 | return new BacktickConsole(); 6 | } 7 | 8 | let open = false, loading = true, searchText = '', 9 | $container, $console, $input, $results, 10 | inputActions = { 11 | 13: enter, 12 | 27: escape, 13 | 38: arrowUp, 14 | 40: arrowDown 15 | }, 16 | documentActions = {}; 17 | 18 | documentActions[store.getHotkey()] = toggle; 19 | 20 | buildHtml(); 21 | 22 | toggle(); 23 | 24 | return { toggle }; 25 | 26 | function inputActionSearch(event) { 27 | 28 | if (loading) { 29 | return; 30 | } 31 | 32 | if (inputActions.hasOwnProperty(event.which)) { 33 | return; 34 | } 35 | 36 | searchText = $input[0].value; 37 | commands.searchCommands(searchText); 38 | } 39 | 40 | function inputAction(event) { 41 | 42 | if (inputActions.hasOwnProperty(event.which)) { 43 | event.preventDefault(); 44 | event.stopPropagation(); 45 | return inputActions[event.which](); 46 | } 47 | } 48 | 49 | function documentAction(event) { 50 | 51 | if (documentActions.hasOwnProperty(event.which)) { 52 | event.preventDefault(); 53 | event.stopPropagation(); 54 | return documentActions[event.which](); 55 | } 56 | } 57 | 58 | // keyActions 59 | 60 | function toggle() { 61 | 62 | $input.val(''); 63 | 64 | if (open) { 65 | 66 | $console.removeClass('in').addClass('out'); 67 | $results.hide(); 68 | $('>input', $console).val(''); 69 | $('>input', $console).blur(); 70 | 71 | } else { 72 | 73 | $('>input', $console).blur(); 74 | $console.removeClass('out').addClass('in'); 75 | setTimeout(() => { $('>input', $console).focus(); }, 500); 76 | } 77 | 78 | open = !open; 79 | loading = false; 80 | } 81 | 82 | function enter() { 83 | commands.runSelected(); 84 | } 85 | 86 | function escape() { 87 | toggle(); 88 | } 89 | 90 | function arrowUp() { 91 | commands.selectPrev(); 92 | } 93 | 94 | function arrowDown() { 95 | commands.selectNext(); 96 | } 97 | 98 | // buildHtml 99 | 100 | function buildHtml() { 101 | 102 | $('body').append(` 103 |
104 |
105 | 106 |
107 | 108 | 109 |
110 | 114 |
115 | `); 116 | 117 | $container = $('#backtick-container'); 118 | 119 | $console = $('.console', $container); 120 | 121 | $input = $('>input', $console); 122 | 123 | $results = $('.results', $container); 124 | 125 | commands.init($results); 126 | 127 | $input.on('keydown', inputAction); 128 | $input.on('keyup', inputActionSearch); 129 | 130 | $('button.settings', $console).on('click', function() { 131 | 132 | chrome.runtime.sendMessage({ 133 | action: 'LoadBacktickChromeTabs', 134 | tabAction: 'create', 135 | params: { 136 | 'url': '/options.html' 137 | } 138 | }, 139 | (response) => { 140 | console.info(response); 141 | }); 142 | }); 143 | 144 | $(document).on('keypress', documentAction); 145 | 146 | if (window.location.host === 'gist.github.com') { 147 | $container.addClass('gist-detected'); 148 | $('button.add-gist', $console).on('click', function() { 149 | return addGist() 150 | .then(rawCommand => { 151 | commands.addItem(rawCommand); 152 | alert('New command added!'); 153 | }) 154 | .catch(error => { 155 | alert(error); 156 | }); 157 | }); 158 | } 159 | 160 | } 161 | } -------------------------------------------------------------------------------- /scripts/options.js: -------------------------------------------------------------------------------- 1 | new BacktickStore().then(backtickStore => { 2 | 3 | let addGist = BacktickAddGistInitiator(backtickStore); 4 | new BacktickOptions(backtickStore, addGist); 5 | }); 6 | 7 | function BacktickOptions(store, addGist) { 8 | 9 | let optionsPage = $('.options-page'), 10 | hotkeyInput = $('#hotkey'), 11 | gistForm = $('#import-form'), 12 | savedList = $('#saved-list'), 13 | libraryList = $('#library-list'), 14 | hotkeyUpdate = setTimeout(() => {}); 15 | 16 | hotkeyInput.val(store.getHotkey() || config.hotkey); 17 | 18 | const loading = { 19 | start: () => { 20 | optionsPage.addClass('loading'); 21 | }, 22 | end: () => { 23 | setTimeout(() => { optionsPage.removeClass('loading'); }, 500); 24 | } 25 | }; 26 | 27 | store.getLibrary().then(libraryCommands => { 28 | 29 | let savedCommands = store.getCommands(); 30 | 31 | libraryList.prepend(libraryCommands.filter(command => !haveSlug(savedCommands, command.slug)) 32 | .map(buildHtml).join('')); 33 | 34 | savedList.prepend(savedCommands 35 | .map(buildHtml).join('')); 36 | 37 | sortCommands(libraryList); 38 | sortCommands(savedList); 39 | 40 | loading.end(); 41 | 42 | libraryList.on('click', '.toggle', function() { 43 | 44 | let toggle = $(this), 45 | element = toggle.closest('li').detach(), 46 | slug = toggle.data('slug'), 47 | command = libraryCommands.filter(command => command.slug === slug)[0]; 48 | 49 | store.addCommands([command]) 50 | .then(commands => { 51 | savedCommands = commands; 52 | element.prependTo(savedList); 53 | sortCommands(savedList); 54 | 55 | loading.end(); 56 | }); 57 | }); 58 | 59 | savedList.on('click', '.toggle', function() { 60 | 61 | let toggle = $(this), 62 | element = toggle.closest('li').detach(), 63 | slug = toggle.data('slug'), 64 | command = savedCommands.filter(command => command.slug === slug)[0]; 65 | 66 | store.removeCommands([command]) 67 | .then(commands => { 68 | savedCommands = commands; 69 | if (haveSlug(libraryCommands, command.slug)) { 70 | element.prependTo(libraryList); 71 | sortCommands(libraryList); 72 | } 73 | loading.end(); 74 | }); 75 | }); 76 | 77 | hotkeyInput 78 | .on('keyup', function() { 79 | hotkeyInput.select(); 80 | clearTimeout(hotkeyUpdate); 81 | loading.start(); 82 | hotkeyUpdate = setTimeout(function() { 83 | store.addHotkey(hotkeyInput.val()).then(() => { 84 | loading.end(); 85 | }); 86 | }, 1000); 87 | }) 88 | .on('click', function() { 89 | hotkeyInput.select(); 90 | }); 91 | 92 | let submitting = false; 93 | gistForm.on('submit', function(event) { 94 | event.preventDefault(); 95 | 96 | let gistId = $('[name="gistId"]', this).val(); 97 | 98 | if (savedCommands.filter(command => command.slug === gistId).length) { 99 | alert('Custom command already added.'); 100 | return false; 101 | } 102 | 103 | if (!submitting && gistId) { 104 | submitting = true; 105 | addGist(gistId) 106 | .then(command => { 107 | savedList.prepend(buildHtml(command)); 108 | sortCommands(savedList); 109 | submitting = false; 110 | alert('New command added!'); 111 | }) 112 | .catch(error => { 113 | alert(error); 114 | }); 115 | } 116 | return false; 117 | }); 118 | 119 | }); 120 | 121 | GoogleAnalytics(); 122 | 123 | function haveSlug(commands, slug) { 124 | return commands.filter(command => command.slug === slug).length; 125 | } 126 | 127 | function sortCommands($list) { 128 | 129 | let $items = $list.children().not('.none'); 130 | 131 | $items.sort(function(a, b) { 132 | 133 | let an = a.getAttribute('data-name'), 134 | bn = b.getAttribute('data-name'); 135 | 136 | return (an > bn) ? 1 : ((an < bn) ? -1 : 0); 137 | }); 138 | 139 | $items.detach().prependTo($list); 140 | } 141 | 142 | function buildHtml(command) { 143 | 144 | command = Object.assign({ link: '', icon: config.defaultCommandIcon }, command); 145 | 146 | return ` 147 |
  • 148 |
    149 |
    150 | ${command.name} 151 |

    ${command.description}

    152 | ` + (command.link ? `${command.link}` : '') + ` 153 |
    154 | 155 | Add 156 | Remove 157 | 158 |
  • 159 | `; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | require('load-grunt-tasks')(grunt); 4 | 5 | const config = { 6 | npmJs: [ 7 | 'node_modules/jquery/dist/jquery.min.js', 8 | 'node_modules/jquery/dist/jquery.min.map' 9 | ] 10 | }; 11 | 12 | const fileName = (path) => path.slice(path.lastIndexOf('/') + 1); 13 | 14 | grunt.initConfig({ 15 | 16 | pkg: grunt.file.readJSON('package.json'), 17 | 18 | clean: { 19 | start: { 20 | src: ['_dist', '.tmp'] 21 | }, 22 | end: { 23 | src: ['.tmp'] 24 | } 25 | }, 26 | 27 | sass: { 28 | dist: { 29 | files: { 30 | 'styles/style.css': 'styles/style.scss' 31 | } 32 | } 33 | }, 34 | 35 | copy: { 36 | main: { 37 | files: [ 38 | { 39 | expand: true, 40 | src: ['fonts/**', 'images/**', 'background.html', 'options.html', 'test.html', 'manifest.json'], 41 | dest: '_dist/' 42 | }, 43 | { 44 | expand: true, 45 | cwd: '.tmp/scripts/', 46 | src: ['*.js', '*.map'], 47 | dest: '_dist/scripts' 48 | }, 49 | { 50 | expand: true, 51 | cwd: 'styles', 52 | src: ['*.css'], 53 | dest: '_dist/styles' 54 | } 55 | ] 56 | .concat(config.npmJs.map(src => ({ src, dest: '_dist/scripts/' + fileName(src) }))) 57 | } 58 | }, 59 | 60 | concat: { 61 | options: { 62 | separator: '\r\n\r\n', 63 | stripBanners: true, 64 | banner: '(function(){\r\n\r\n', 65 | footer: '\r\n\r\n\r\n\r\n})();' 66 | }, 67 | main: { 68 | files: [ 69 | { 70 | src: [ 71 | 'scripts/config.js', 72 | 'scripts/lib/add-gist.js', 73 | 'scripts/lib/commands.js', 74 | 'scripts/lib/console.js', 75 | 'scripts/lib/store.js', 76 | 'scripts/main.js' 77 | ], 78 | dest: '.tmp/scripts/main.js' 79 | }, 80 | { 81 | src: [ 82 | 'scripts/config.js', 83 | 'scripts/lib/analytics.js', 84 | 'scripts/lib/add-gist.js', 85 | 'scripts/lib/store.js', 86 | 'scripts/options.js' 87 | ], 88 | dest: '.tmp/scripts/options.js' 89 | } 90 | ] 91 | } 92 | }, 93 | 94 | babel: { 95 | options: { 96 | sourceMap: true, 97 | presets: ['es2015'], 98 | plugins: [ 99 | ['transform-class-properties', { 'spec': true }] 100 | ] 101 | }, 102 | dist: { 103 | files: [ 104 | { 105 | expand: true, 106 | cwd: 'scripts/', 107 | src: ['background.js', 'content-script.js', 'test.js'], 108 | dest: '.tmp/scripts/' 109 | }, 110 | { 111 | expand: true, 112 | cwd: '.tmp/scripts/', 113 | src: ['main.js', 'options.js'], 114 | dest: '.tmp/scripts/' 115 | } 116 | ] 117 | } 118 | }, 119 | 120 | uglify: { 121 | stick: { 122 | options: { 123 | sourceMap: true, 124 | sourceMapName: '.tmp/scripts/main.min.map' 125 | }, 126 | files: { 127 | '.tmp/scripts/main.min.js': ['.tmp/scripts/main.js'] 128 | } 129 | } 130 | }, 131 | 132 | watch: { 133 | scripts: { 134 | files: ['scripts/*.js', 'scripts/**/*.js'], 135 | tasks: [ 136 | 'concat', 137 | 'babel', 138 | //'uglify', 139 | 'copy', 140 | 'clean:end' 141 | ], 142 | options: { 143 | spawn: false 144 | } 145 | }, 146 | styles: { 147 | files: ['styles/*.scss', 'styles/**/*.scss'], 148 | tasks: ['sass', 'copy'], 149 | options: { 150 | spawn: false 151 | } 152 | }, 153 | others: { 154 | files: ['fonts/**', 'images/**', 'background.html', 'test.html', 'options.html', 'manifest.json'], 155 | tasks: ['copy'], 156 | options: { 157 | spawn: false 158 | } 159 | } 160 | } 161 | }); 162 | 163 | grunt.loadNpmTasks('grunt-contrib-uglify'); 164 | grunt.loadNpmTasks('grunt-contrib-concat'); 165 | grunt.loadNpmTasks('grunt-contrib-watch'); 166 | grunt.loadNpmTasks('grunt-contrib-clean'); 167 | grunt.loadNpmTasks('grunt-contrib-copy'); 168 | grunt.loadNpmTasks('grunt-sass'); 169 | 170 | grunt.registerTask('default', [ 171 | 'clean:start', 172 | 'sass', 173 | 'concat', 174 | 'babel', 175 | //'uglify', 176 | 'copy', 177 | 'clean:end' 178 | ]); 179 | }; -------------------------------------------------------------------------------- /scripts/lib/commands.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | function BacktickCommands(store) { 3 | 4 | let commandItems = [], $results, $resultsContainer, loaded = false; 5 | const resultsHeight = 275; 6 | 7 | commandItems.showing = () => commandItems.filter(command => command.isShowing()); 8 | 9 | commandItems.selected = () => commandItems.filter(command => command.isSelected()); 10 | 11 | return { 12 | init, 13 | addItem, 14 | searchCommands, 15 | selectNext: select('next'), 16 | selectPrev: select('prev'), 17 | runSelected, 18 | loadCommands 19 | }; 20 | 21 | function runSelected() { 22 | 23 | let selected = commandItems.selected(); 24 | selected.length && selected[0].run(); 25 | } 26 | 27 | function select(action) { 28 | return function() { 29 | 30 | let selected = commandItems.selected(); 31 | 32 | if ('run' === action && selected.length) { 33 | selected[0].run(); 34 | } 35 | 36 | let showing = commandItems.showing(); 37 | 38 | if (!loaded) { 39 | $results.show(); 40 | loaded = true; 41 | return showing[0].setSelected(); 42 | } 43 | 44 | if (!showing.length) { 45 | return; 46 | } 47 | 48 | if ('first' === action) { 49 | return showing[0].setSelected(); 50 | } 51 | 52 | if (!showing.filter(command => command.isSelected()).length) { 53 | return showing[0].setSelected(); 54 | } 55 | 56 | for (let index = 0; index < showing.length; index++) { 57 | 58 | if (showing[index].isSelected()) { 59 | 60 | if ('next' === action) { 61 | let selectIndex = (index + 1) < showing.length ? (index + 1) : 0; 62 | return showing[selectIndex].setSelected(); 63 | } 64 | 65 | if ('prev' === action) { 66 | let selectIndex = (index - 1) > -1 ? (index - 1) : showing.length - 1; 67 | return showing[selectIndex].setSelected(); 68 | } 69 | } 70 | } 71 | }; 72 | } 73 | 74 | function loadCommands() { 75 | 76 | $resultsContainer = $('ul', $results).empty(); 77 | 78 | commandItems.forEach(commandItem => commandItem.$element.remove()); 79 | 80 | commandItems.length = 0; 81 | 82 | return Promise.resolve(store.getCommands()) 83 | .then(initializeItems); 84 | } 85 | 86 | function init() { 87 | 88 | $results = arguments[0]; 89 | 90 | return loadCommands(); 91 | } 92 | 93 | function addItem(rawCommand) { 94 | 95 | let command = new BacktickCommand(rawCommand, true); 96 | 97 | command.setSelected(); 98 | 99 | return Promise.resolve(true); 100 | } 101 | 102 | function initializeItems(rawCommands) { 103 | 104 | rawCommands.forEach(rawCommand => new BacktickCommand(rawCommand)); 105 | 106 | return Promise.resolve(commandItems); 107 | } 108 | 109 | function BacktickCommand(rawCommand, prepend) { 110 | 111 | if (!rawCommand) { 112 | return {}; 113 | } 114 | 115 | let selected = false, 116 | showing = true, 117 | command = { 118 | height: 0, 119 | index: commandItems.length, 120 | $element: $(buildHtml(rawCommand)), 121 | isSelected, 122 | isShowing, 123 | setShow, 124 | setSelected, 125 | run, 126 | getHeight 127 | }; 128 | 129 | const blob = [rawCommand.name, rawCommand.description, rawCommand.link].join(' ').toLowerCase(); 130 | 131 | if (prepend) { 132 | commandItems.unshift(command); 133 | command.$element.prependTo($resultsContainer); 134 | commandItems.forEach((cmd, index) => { cmd.index = index; }); 135 | } else { 136 | commandItems.push(command); 137 | command.$element.appendTo($resultsContainer); 138 | } 139 | 140 | command.$element.on('click', () => { 141 | command.setSelected(); 142 | command.$element.trigger('run.backtick.command.please'); 143 | }); 144 | 145 | command.$element.on('run.backtick.command.please', command.run); 146 | 147 | return command; 148 | 149 | function run() { 150 | chrome.runtime.sendMessage({ action: 'LoadBacktickCommand', script: rawCommand.command }); 151 | } 152 | 153 | function getHeight() { 154 | if (!command.height) { 155 | command.height = command.$element.outerHeight(); 156 | } 157 | return command.height; 158 | } 159 | 160 | function setSelected(selectCommand) { 161 | 162 | if (selectCommand === false) { 163 | command.$element.removeClass('selected'); 164 | selected = false; 165 | return selected; 166 | } 167 | 168 | commandItems.forEach(command => command.setSelected(false)); 169 | command.$element.addClass('selected').focus(); 170 | 171 | $results.scrollTop(scrollTo()); 172 | 173 | selected = true; 174 | return selected; 175 | } 176 | 177 | function scrollTo() { 178 | 179 | const showingCommands = commandItems.showing(); 180 | 181 | if (showingCommands[0].index === command.index) { 182 | return 0; 183 | } 184 | 185 | const itemTop = showingCommands 186 | .map(cmd => { 187 | return cmd.index < command.index ? cmd.getHeight() : 0; 188 | }) 189 | .concat([0]) 190 | .reduce(function(a, b) { return a + b; }); 191 | 192 | if (showingCommands[showingCommands.length - 1].index === command.index) { 193 | return itemTop; 194 | } 195 | 196 | const 197 | itemBottom = itemTop + command.getHeight(), 198 | windowTop = $results.scrollTop(), 199 | windowBottom = windowTop + resultsHeight; 200 | 201 | const 202 | withinTop = itemTop >= windowTop, 203 | withinBottom = itemBottom <= windowBottom; 204 | 205 | if (!withinTop || !withinBottom) { 206 | 207 | let increment = (!withinTop ? -1 : 1); 208 | return windowTop + (increment * command.getHeight()); 209 | } 210 | } 211 | 212 | function isSelected() { 213 | return selected; 214 | } 215 | 216 | function isShowing() { 217 | return showing; 218 | } 219 | 220 | function setShow(searchValue) { 221 | 222 | let hasValue = blob.includes(searchValue); 223 | 224 | showing = !searchValue || hasValue; 225 | 226 | command.$element.toggle(showing); 227 | 228 | if (showing) { 229 | $('.name,.description,.link', command.$element).each((index, propertyElement) => { 230 | 231 | let text = rawCommand[propertyElement.className]; 232 | if (text) { 233 | 234 | let $propertyElement = $(propertyElement); 235 | $propertyElement.text(text); 236 | if (hasValue) { 237 | $propertyElement.html(text.replace(new RegExp(searchValue, 'gi'), 238 | (match) => '' + match + '')); 239 | } 240 | } 241 | }); 242 | command.$element.show(); 243 | return true; 244 | } 245 | 246 | command.$element.hide(); 247 | selected = false; 248 | return false; 249 | } 250 | 251 | function buildHtml(command) { 252 | command = Object.assign({ link: '', icon: config.defaultCommandIcon }, command); 253 | return ` 254 |
  • 255 |
    256 |
    257 | ${command.name} 258 |

    ${command.description}

    259 | ` + (command.link ? `${command.link}` : '') + ` 260 |
    261 |
  • `; 262 | } 263 | } 264 | 265 | function searchCommands(searchText) { 266 | 267 | loaded = true; 268 | 269 | searchText = (searchText || '').toLowerCase().trim(); 270 | 271 | commandItems.forEach(command => command.setShow(searchText)); 272 | 273 | let isShowingCommands = commandItems.showing().length > 0; 274 | 275 | $results.toggle(isShowingCommands); 276 | 277 | select('first')(); 278 | 279 | return isShowingCommands; 280 | } 281 | } -------------------------------------------------------------------------------- /styles/style.scss: -------------------------------------------------------------------------------- 1 | $root-url: 'chrome-extension://__MSG_@@extension_id__'; 2 | 3 | @font-face { 4 | font-family: 'Lato'; 5 | font-style: normal; 6 | font-weight: 300; 7 | src: local('Lato Light'), local('Lato-Light'), 8 | url($root-url + '/fonts/Lato-Light.ttf') format('woff'), 9 | url($root-url + '/fonts/Lato-Light.ttf') format('woff'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Lato'; 14 | font-style: normal; 15 | font-weight: 400; 16 | src: local('Lato Regular'), local('Lato-Regular'), 17 | url($root-url + '/fonts/Lato-Regular.ttf') format('woff'), 18 | url($root-url + '/fonts/Lato-Regular.ttf') format('woff'); 19 | } 20 | 21 | @font-face { 22 | font-family: 'Lato'; 23 | font-style: normal; 24 | font-weight: 700; 25 | src: local('Lato Bold'), local('Lato-Bold'), 26 | url($root-url + '/fonts/Lato-Bold.ttf') format('woff'), 27 | url($root-url + '/fonts/Lato-Bold.ttf') format('woff'); 28 | } 29 | 30 | @font-face { 31 | font-family: 'Lato'; 32 | font-style: italic; 33 | font-weight: 400; 34 | src: local('Lato Italic'), local('Lato-Italic'), 35 | url($root-url + '/fonts/Lato-Italic.ttf') format('woff'), 36 | url($root-url + '/fonts/Lato-Italic.ttf') format('woff'); 37 | } 38 | 39 | $background-color: #362D26; 40 | $foreground-color: #fef8f0; 41 | $accent-color: #ff9900; 42 | 43 | $fonts: "Lato", sans-serif; 44 | 45 | #backtick-container { 46 | 47 | display: block; 48 | position: fixed; 49 | z-index: 2147483647; 50 | 51 | left: 20px; 52 | top: 20px; 53 | 54 | width: 400px; 55 | 56 | font-family: "Lato", sans-serif; 57 | font-size: 16px; 58 | text-align: left; 59 | line-height: normal; 60 | color: $foreground-color; 61 | 62 | * { 63 | box-sizing: border-box; 64 | } 65 | 66 | .custom-scrollbar { 67 | &::-webkit-scrollbar { 68 | width: 5px; 69 | } 70 | 71 | &::-webkit-scrollbar-track { 72 | background: none; 73 | } 74 | 75 | &::-webkit-scrollbar-thumb { 76 | background: fade-out($foreground-color, 0.7); 77 | } 78 | } 79 | 80 | @import "animations"; 81 | 82 | .console { 83 | display: block; 84 | width: 100%; 85 | height: 50px; 86 | padding: 5px 10px; 87 | 88 | background-color: $background-color; 89 | 90 | opacity: 0; 91 | 92 | &.in { 93 | -webkit-animation: _bt-console-in 0.3s both; 94 | } 95 | 96 | &.out { 97 | -webkit-animation: _bt-console-out 0.3s both; 98 | } 99 | 100 | button.settings { 101 | position: relative; 102 | display: inline-block; 103 | vertical-align: top; 104 | 105 | width: 40px; 106 | height: 40px; 107 | margin-right: 5px; 108 | 109 | background: url($root-url + "/images/icon.png") no-repeat; 110 | background-size: 100%; 111 | background-color: inherit; 112 | 113 | border: none; 114 | opacity: 0.8; 115 | 116 | cursor: pointer; 117 | transition: opacity 0.25s; 118 | 119 | &:after { 120 | display: block; 121 | content: ""; 122 | position: absolute; 123 | 124 | left: 0; 125 | top: 0; 126 | 127 | width: 100%; 128 | height: 100%; 129 | 130 | background-image: url($root-url + "/images/icon-highlight.png"); 131 | background-size: 100%; 132 | background-color: inherit; 133 | opacity: 0; 134 | 135 | transition: opacity 0.25s; 136 | } 137 | 138 | &:hover { 139 | opacity: 1; 140 | &:after { 141 | opacity: 1; 142 | } 143 | } 144 | 145 | &:active { 146 | top: 1px; 147 | } 148 | 149 | &:focus { 150 | outline: none; 151 | } 152 | } 153 | 154 | .spinner { 155 | display: none; 156 | position: absolute; 157 | top: 5px; 158 | left: 15px; 159 | width: 30px; 160 | height: 40px; 161 | 162 | opacity: 0.8; 163 | 164 | background: url($root-url + "/images/spinner.png") repeat-x; 165 | background-size: auto 100%; 166 | } 167 | 168 | &.loading { 169 | .spinner { 170 | display: block; 171 | -webkit-animation: _bt-fade-in 0.5s 0.25s both, 172 | _bt-spinner 0.5s infinite linear both; 173 | } 174 | 175 | button.settings { 176 | opacity: 0; 177 | pointer-events: none; 178 | } 179 | } 180 | 181 | button.add-gist { 182 | display: none; 183 | } 184 | 185 | input { 186 | display: inline-block; 187 | width: 330px; 188 | height: 100%; 189 | 190 | background: transparent; 191 | border: none; 192 | 193 | font-family: $fonts; 194 | font-size: 16px; 195 | line-height: 18px; 196 | 197 | color: $foreground-color !important; 198 | 199 | &:focus { 200 | outline: none; 201 | } 202 | 203 | &::-webkit-input-placeholder { 204 | font-style: italic !important; 205 | color: mix($foreground-color, $background-color, 50%) !important; 206 | } 207 | } 208 | } 209 | 210 | &.gist-detected { 211 | 212 | .console { 213 | 214 | button.add-gist { 215 | position: relative; 216 | display: inline-block; 217 | vertical-align: top; 218 | 219 | width: 30px; 220 | height: 40px; 221 | margin-left: 5px; 222 | padding: 0; 223 | 224 | background: url($root-url + "/images/add-icon.png") no-repeat center center; 225 | background-size: 100%; 226 | background-color: inherit; 227 | 228 | border: none; 229 | opacity: 0.8; 230 | 231 | cursor: pointer; 232 | transition: opacity 0.25s; 233 | 234 | &:hover { 235 | opacity: 1; 236 | &:after { 237 | opacity: 1; 238 | } 239 | } 240 | 241 | &:active { 242 | top: 1px; 243 | } 244 | 245 | &:focus { 246 | outline: none; 247 | } 248 | } 249 | 250 | input { 251 | width: 293px; 252 | } 253 | } 254 | } 255 | 256 | .results { 257 | width: 100%; 258 | display: block; 259 | max-height: 275px; 260 | overflow: auto; 261 | background-color: $background-color; 262 | position: relative; 263 | 264 | * { 265 | cursor: default; 266 | -webkit-user-select: none; 267 | user-select: none; 268 | } 269 | 270 | ul { 271 | display: block; 272 | padding: 0 0 10px; 273 | margin: 0; 274 | } 275 | 276 | .command { 277 | &.selected { 278 | background-color: rgba(255, 255, 255, 0.095); 279 | } 280 | 281 | &:active, &.active { 282 | background-color: rgba(255, 255, 255, 0.1); 283 | 284 | .icon { 285 | margin-top: 6px; 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | .command { 293 | display: block; 294 | padding: 6px 10px; 295 | margin: 0; 296 | 297 | .icon { 298 | display: inline-block; 299 | vertical-align: top; 300 | height: 30px; 301 | width: 30px; 302 | margin-top: 4px; 303 | border-radius: 3px; 304 | margin-left: 5px; 305 | 306 | background: url($root-url + "/images/command-default.png") no-repeat; 307 | background-size: 100%; 308 | } 309 | 310 | .body { 311 | display: inline-block; 312 | margin-left: 10px; 313 | width: 325px; 314 | 315 | .name { 316 | display: block; 317 | 318 | .match { 319 | font-weight: bold; 320 | color: $accent-color; 321 | } 322 | } 323 | 324 | .description { 325 | margin: 2px 0; 326 | opacity: 0.8; 327 | font-size: 14px; 328 | font-style: italic; 329 | &:empty { 330 | display: none; 331 | } 332 | .match { 333 | font-weight: bold; 334 | color: $accent-color; 335 | } 336 | } 337 | 338 | a.link, a.link:visited { 339 | display: inline-block; 340 | width: auto; 341 | font-size: 12px; 342 | margin-top: 3px; 343 | opacity: 0.5; 344 | cursor: pointer; 345 | color: $foreground-color; 346 | text-decoration: none; 347 | &:empty { 348 | display: none; 349 | } 350 | &:hover { 351 | text-decoration: underline; 352 | color: $accent-color; 353 | opacity: 1; 354 | } 355 | 356 | .match { 357 | font-weight: bold; 358 | color: $accent-color; 359 | } 360 | } 361 | } 362 | } 363 | 364 | .command + .none { 365 | display: none; 366 | } 367 | 368 | .options-page { 369 | color: $foreground-color; 370 | background-color: $background-color; 371 | 372 | font-family: "Lato", sans-serif; 373 | font-size: 16px; 374 | text-align: center; 375 | 376 | * { 377 | box-sizing: border-box; 378 | } 379 | 380 | h1, h2 { 381 | font-weight: 300; 382 | margin-bottom: 0.25em; 383 | } 384 | 385 | h1 + p, h2 + p { 386 | margin: 0.25em 0 1em; 387 | } 388 | 389 | a, a:visited { 390 | color: $accent-color; 391 | text-decoration: none; 392 | 393 | &:hover { 394 | text-decoration: underline; 395 | } 396 | } 397 | 398 | .button, a.button { 399 | -webkit-appearance: none; 400 | border: none; 401 | outline: none; 402 | box-shadow: none; 403 | 404 | padding: 15px 20px; 405 | 406 | font-size: 16px; 407 | background-color: fade_out($accent-color, 0.1); 408 | color: white; 409 | 410 | cursor: pointer; 411 | transition: background-color 0.15s; 412 | 413 | text-decoration: none; 414 | 415 | &:hover { 416 | background-color: $accent-color; 417 | text-decoration: none; 418 | } 419 | 420 | &:active { 421 | position: relative; 422 | top: 1px; 423 | } 424 | } 425 | 426 | input[type="text"] { 427 | font-family: "Lato", sans-serif; 428 | 429 | background-color: $background-color; 430 | color: $foreground-color; 431 | 432 | border: none; 433 | outline: none; 434 | box-shadow: none; 435 | } 436 | 437 | .container { 438 | width: 520px; 439 | margin: 0 auto; 440 | } 441 | 442 | .section { 443 | width: 100%; 444 | background: rgba(0, 0, 0, 0.1); 445 | padding: 20px 30px; 446 | margin: 20px auto; 447 | 448 | h2 { 449 | margin-top: 0; 450 | } 451 | 452 | small, p { 453 | opacity: 0.7; 454 | } 455 | } 456 | 457 | .hotkey-container { 458 | label { 459 | vertical-align: middle; 460 | font-size: 26px; 461 | font-weight: 300; 462 | opacity: 0.8; 463 | } 464 | 465 | input { 466 | width: 70px; 467 | height: 70px; 468 | padding: 10px; 469 | margin-left: 20px; 470 | vertical-align: middle; 471 | 472 | font-size: 30px; 473 | text-align: center; 474 | font-weight: bold; 475 | &::selection { 476 | background-color: rgba(255, 255, 255, 0.075); 477 | } 478 | } 479 | 480 | small { 481 | display: block; 482 | margin-top: 20px; 483 | font-size: 14px; 484 | text-align: left; 485 | line-height: 1.6em; 486 | } 487 | } 488 | 489 | .commands-container { 490 | 491 | input[type=text] { 492 | width: 370px; 493 | font-size: 16px; 494 | padding: 15px 18px; 495 | 496 | &::-webkit-input-placeholder { 497 | color: mix($foreground-color, $background-color, 30%) !important; 498 | } 499 | &::selection { 500 | background-color: rgba(255, 255, 255, 0.075); 501 | } 502 | } 503 | 504 | input[type=submit] { 505 | margin-left: 10px; 506 | width: 80px; 507 | padding-left: 0; 508 | padding-right: 0; 509 | } 510 | 511 | .learn { 512 | font-size: 14px; 513 | opacity: 0.7; 514 | margin-top: 5px; 515 | margin-left: 5px; 516 | } 517 | 518 | ul { 519 | padding-left: 0; 520 | margin-bottom: 10px; 521 | max-height: 290px; 522 | overflow: auto; 523 | } 524 | 525 | #saved-list { 526 | .toggle { 527 | .remove { 528 | display: inline; 529 | } 530 | .add { 531 | display: none; 532 | } 533 | } 534 | } 535 | 536 | #library-list { 537 | .toggle { 538 | .remove { 539 | display: none; 540 | } 541 | .add { 542 | display: inline; 543 | } 544 | } 545 | } 546 | 547 | .command { 548 | position: relative; 549 | text-align: left; 550 | margin-bottom: 5px; 551 | padding: 10px; 552 | 553 | transition: background-color 0.15s; 554 | 555 | small { 556 | opacity: 0.5; 557 | margin-left: 3px; 558 | } 559 | 560 | .toggle { 561 | text-transform: capitalize; 562 | display: block; 563 | position: absolute; 564 | right: 10px; 565 | top: 5px; 566 | 567 | font-size: 14px; 568 | color: $accent-color; 569 | cursor: pointer; 570 | opacity: 0; 571 | 572 | transition: opacity 0.15s; 573 | 574 | &:hover { 575 | text-decoration: underline; 576 | } 577 | } 578 | 579 | &:hover { 580 | background-color: rgba(255, 255, 255, 0.05); 581 | 582 | .toggle { 583 | opacity: 0.7; 584 | } 585 | } 586 | } 587 | } 588 | 589 | .credits small { 590 | font-size: 14px; 591 | } 592 | 593 | .spinner { 594 | position: fixed; 595 | bottom: 5px; 596 | right: 15px; 597 | width: 30px; 598 | height: 40px; 599 | opacity: 0; 600 | background: url($root-url + "/images/spinner.png") repeat-x; 601 | background-size: auto 100%; 602 | transition: opacity 0.5s; 603 | -webkit-animation: _bt-spinner 0.5s infinite linear both; 604 | } 605 | 606 | &.loading .spinner { 607 | opacity: 0.5; 608 | transition: opacity 0.5s 0.2s; 609 | } 610 | } 611 | 612 | #__backtick__ { 613 | pointer-events: none; 614 | background: transparent !important; 615 | 616 | &.open { 617 | pointer-events: all; 618 | } 619 | &:focus { 620 | outline: none; 621 | } 622 | } -------------------------------------------------------------------------------- /styles/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local("Lato Light"), local("Lato-Light"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Light.ttf") format("woff"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Light.ttf") format("woff"); } 6 | 7 | @font-face { 8 | font-family: 'Lato'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local("Lato Regular"), local("Lato-Regular"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Regular.ttf") format("woff"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Regular.ttf") format("woff"); } 12 | 13 | @font-face { 14 | font-family: 'Lato'; 15 | font-style: normal; 16 | font-weight: 700; 17 | src: local("Lato Bold"), local("Lato-Bold"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Bold.ttf") format("woff"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Bold.ttf") format("woff"); } 18 | 19 | @font-face { 20 | font-family: 'Lato'; 21 | font-style: italic; 22 | font-weight: 400; 23 | src: local("Lato Italic"), local("Lato-Italic"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Italic.ttf") format("woff"), url("chrome-extension://__MSG_@@extension_id__/fonts/Lato-Italic.ttf") format("woff"); } 24 | 25 | #backtick-container { 26 | display: block; 27 | position: fixed; 28 | z-index: 2147483647; 29 | left: 20px; 30 | top: 20px; 31 | width: 400px; 32 | font-family: "Lato", sans-serif; 33 | font-size: 16px; 34 | text-align: left; 35 | line-height: normal; 36 | color: #fef8f0; } 37 | #backtick-container * { 38 | box-sizing: border-box; } 39 | #backtick-container .custom-scrollbar::-webkit-scrollbar { 40 | width: 5px; } 41 | #backtick-container .custom-scrollbar::-webkit-scrollbar-track { 42 | background: none; } 43 | #backtick-container .custom-scrollbar::-webkit-scrollbar-thumb { 44 | background: rgba(254, 248, 240, 0.3); } 45 | 46 | @-webkit-keyframes _bt-console-in { 47 | 0% { 48 | opacity: 0; 49 | -webkit-transform: translate3D(0, -100px, 0); } 50 | 100% { 51 | opacity: 1; } } 52 | 53 | @-webkit-keyframes _bt-console-out { 54 | 0% { 55 | opacity: 1; } 56 | 100% { 57 | opacity: 0; 58 | -webkit-transform: translate3D(0, -100px, 0); } } 59 | 60 | @-webkit-keyframes _bt-spinner { 61 | 100% { 62 | background-position-x: -100%; } } 63 | 64 | @-webkit-keyframes _bt-fade-in { 65 | 0% { 66 | opacity: 0; } } 67 | 68 | @-webkit-keyframes _bt-fade-out { 69 | 100% { 70 | opacity: 0; } } 71 | #backtick-container .console { 72 | display: block; 73 | width: 100%; 74 | height: 50px; 75 | padding: 5px 10px; 76 | background-color: #362D26; 77 | opacity: 0; } 78 | #backtick-container .console.in { 79 | -webkit-animation: _bt-console-in 0.3s both; } 80 | #backtick-container .console.out { 81 | -webkit-animation: _bt-console-out 0.3s both; } 82 | #backtick-container .console button.settings { 83 | position: relative; 84 | display: inline-block; 85 | vertical-align: top; 86 | width: 40px; 87 | height: 40px; 88 | margin-right: 5px; 89 | background: url("chrome-extension://__MSG_@@extension_id__/images/icon.png") no-repeat; 90 | background-size: 100%; 91 | background-color: inherit; 92 | border: none; 93 | opacity: 0.8; 94 | cursor: pointer; 95 | transition: opacity 0.25s; } 96 | #backtick-container .console button.settings:after { 97 | display: block; 98 | content: ""; 99 | position: absolute; 100 | left: 0; 101 | top: 0; 102 | width: 100%; 103 | height: 100%; 104 | background-image: url("chrome-extension://__MSG_@@extension_id__/images/icon-highlight.png"); 105 | background-size: 100%; 106 | background-color: inherit; 107 | opacity: 0; 108 | transition: opacity 0.25s; } 109 | #backtick-container .console button.settings:hover { 110 | opacity: 1; } 111 | #backtick-container .console button.settings:hover:after { 112 | opacity: 1; } 113 | #backtick-container .console button.settings:active { 114 | top: 1px; } 115 | #backtick-container .console button.settings:focus { 116 | outline: none; } 117 | #backtick-container .console .spinner { 118 | display: none; 119 | position: absolute; 120 | top: 5px; 121 | left: 15px; 122 | width: 30px; 123 | height: 40px; 124 | opacity: 0.8; 125 | background: url("chrome-extension://__MSG_@@extension_id__/images/spinner.png") repeat-x; 126 | background-size: auto 100%; } 127 | #backtick-container .console.loading .spinner { 128 | display: block; 129 | -webkit-animation: _bt-fade-in 0.5s 0.25s both, _bt-spinner 0.5s infinite linear both; } 130 | #backtick-container .console.loading button.settings { 131 | opacity: 0; 132 | pointer-events: none; } 133 | #backtick-container .console button.add-gist { 134 | display: none; } 135 | #backtick-container .console input { 136 | display: inline-block; 137 | width: 330px; 138 | height: 100%; 139 | background: transparent; 140 | border: none; 141 | font-family: "Lato", sans-serif; 142 | font-size: 16px; 143 | line-height: 18px; 144 | color: #fef8f0 !important; } 145 | #backtick-container .console input:focus { 146 | outline: none; } 147 | #backtick-container .console input::-webkit-input-placeholder { 148 | font-style: italic !important; 149 | color: #9a938b !important; } 150 | #backtick-container.gist-detected .console button.add-gist { 151 | position: relative; 152 | display: inline-block; 153 | vertical-align: top; 154 | width: 30px; 155 | height: 40px; 156 | margin-left: 5px; 157 | padding: 0; 158 | background: url("chrome-extension://__MSG_@@extension_id__/images/add-icon.png") no-repeat center center; 159 | background-size: 100%; 160 | background-color: inherit; 161 | border: none; 162 | opacity: 0.8; 163 | cursor: pointer; 164 | transition: opacity 0.25s; } 165 | #backtick-container.gist-detected .console button.add-gist:hover { 166 | opacity: 1; } 167 | #backtick-container.gist-detected .console button.add-gist:hover:after { 168 | opacity: 1; } 169 | #backtick-container.gist-detected .console button.add-gist:active { 170 | top: 1px; } 171 | #backtick-container.gist-detected .console button.add-gist:focus { 172 | outline: none; } 173 | #backtick-container.gist-detected .console input { 174 | width: 293px; } 175 | #backtick-container .results { 176 | width: 100%; 177 | display: block; 178 | max-height: 275px; 179 | overflow: auto; 180 | background-color: #362D26; 181 | position: relative; } 182 | #backtick-container .results * { 183 | cursor: default; 184 | -webkit-user-select: none; 185 | user-select: none; } 186 | #backtick-container .results ul { 187 | display: block; 188 | padding: 0 0 10px; 189 | margin: 0; } 190 | #backtick-container .results .command.selected { 191 | background-color: rgba(255, 255, 255, 0.095); } 192 | #backtick-container .results .command:active, #backtick-container .results .command.active { 193 | background-color: rgba(255, 255, 255, 0.1); } 194 | #backtick-container .results .command:active .icon, #backtick-container .results .command.active .icon { 195 | margin-top: 6px; } 196 | 197 | .command { 198 | display: block; 199 | padding: 6px 10px; 200 | margin: 0; } 201 | .command .icon { 202 | display: inline-block; 203 | vertical-align: top; 204 | height: 30px; 205 | width: 30px; 206 | margin-top: 4px; 207 | border-radius: 3px; 208 | margin-left: 5px; 209 | background: url("chrome-extension://__MSG_@@extension_id__/images/command-default.png") no-repeat; 210 | background-size: 100%; } 211 | .command .body { 212 | display: inline-block; 213 | margin-left: 10px; 214 | width: 325px; } 215 | .command .body .name { 216 | display: block; } 217 | .command .body .name .match { 218 | font-weight: bold; 219 | color: #ff9900; } 220 | .command .body .description { 221 | margin: 2px 0; 222 | opacity: 0.8; 223 | font-size: 14px; 224 | font-style: italic; } 225 | .command .body .description:empty { 226 | display: none; } 227 | .command .body .description .match { 228 | font-weight: bold; 229 | color: #ff9900; } 230 | .command .body a.link, .command .body a.link:visited { 231 | display: inline-block; 232 | width: auto; 233 | font-size: 12px; 234 | margin-top: 3px; 235 | opacity: 0.5; 236 | cursor: pointer; 237 | color: #fef8f0; 238 | text-decoration: none; } 239 | .command .body a.link:empty, .command .body a.link:visited:empty { 240 | display: none; } 241 | .command .body a.link:hover, .command .body a.link:visited:hover { 242 | text-decoration: underline; 243 | color: #ff9900; 244 | opacity: 1; } 245 | .command .body a.link .match, .command .body a.link:visited .match { 246 | font-weight: bold; 247 | color: #ff9900; } 248 | 249 | .command + .none { 250 | display: none; } 251 | 252 | .options-page { 253 | color: #fef8f0; 254 | background-color: #362D26; 255 | font-family: "Lato", sans-serif; 256 | font-size: 16px; 257 | text-align: center; } 258 | .options-page * { 259 | box-sizing: border-box; } 260 | .options-page h1, .options-page h2 { 261 | font-weight: 300; 262 | margin-bottom: 0.25em; } 263 | .options-page h1 + p, .options-page h2 + p { 264 | margin: 0.25em 0 1em; } 265 | .options-page a, .options-page a:visited { 266 | color: #ff9900; 267 | text-decoration: none; } 268 | .options-page a:hover, .options-page a:visited:hover { 269 | text-decoration: underline; } 270 | .options-page .button, .options-page a.button { 271 | -webkit-appearance: none; 272 | border: none; 273 | outline: none; 274 | box-shadow: none; 275 | padding: 15px 20px; 276 | font-size: 16px; 277 | background-color: rgba(255, 153, 0, 0.9); 278 | color: white; 279 | cursor: pointer; 280 | transition: background-color 0.15s; 281 | text-decoration: none; } 282 | .options-page .button:hover, .options-page a.button:hover { 283 | background-color: #ff9900; 284 | text-decoration: none; } 285 | .options-page .button:active, .options-page a.button:active { 286 | position: relative; 287 | top: 1px; } 288 | .options-page input[type="text"] { 289 | font-family: "Lato", sans-serif; 290 | background-color: #362D26; 291 | color: #fef8f0; 292 | border: none; 293 | outline: none; 294 | box-shadow: none; } 295 | .options-page .container { 296 | width: 520px; 297 | margin: 0 auto; } 298 | .options-page .section { 299 | width: 100%; 300 | background: rgba(0, 0, 0, 0.1); 301 | padding: 20px 30px; 302 | margin: 20px auto; } 303 | .options-page .section h2 { 304 | margin-top: 0; } 305 | .options-page .section small, .options-page .section p { 306 | opacity: 0.7; } 307 | .options-page .hotkey-container label { 308 | vertical-align: middle; 309 | font-size: 26px; 310 | font-weight: 300; 311 | opacity: 0.8; } 312 | .options-page .hotkey-container input { 313 | width: 70px; 314 | height: 70px; 315 | padding: 10px; 316 | margin-left: 20px; 317 | vertical-align: middle; 318 | font-size: 30px; 319 | text-align: center; 320 | font-weight: bold; } 321 | .options-page .hotkey-container input::selection { 322 | background-color: rgba(255, 255, 255, 0.075); } 323 | .options-page .hotkey-container small { 324 | display: block; 325 | margin-top: 20px; 326 | font-size: 14px; 327 | text-align: left; 328 | line-height: 1.6em; } 329 | .options-page .commands-container input[type=text] { 330 | width: 370px; 331 | font-size: 16px; 332 | padding: 15px 18px; } 333 | .options-page .commands-container input[type=text]::-webkit-input-placeholder { 334 | color: #726a63 !important; } 335 | .options-page .commands-container input[type=text]::selection { 336 | background-color: rgba(255, 255, 255, 0.075); } 337 | .options-page .commands-container input[type=submit] { 338 | margin-left: 10px; 339 | width: 80px; 340 | padding-left: 0; 341 | padding-right: 0; } 342 | .options-page .commands-container .learn { 343 | font-size: 14px; 344 | opacity: 0.7; 345 | margin-top: 5px; 346 | margin-left: 5px; } 347 | .options-page .commands-container ul { 348 | padding-left: 0; 349 | margin-bottom: 10px; 350 | max-height: 290px; 351 | overflow: auto; } 352 | .options-page .commands-container #saved-list .toggle .remove { 353 | display: inline; } 354 | .options-page .commands-container #saved-list .toggle .add { 355 | display: none; } 356 | .options-page .commands-container #library-list .toggle .remove { 357 | display: none; } 358 | .options-page .commands-container #library-list .toggle .add { 359 | display: inline; } 360 | .options-page .commands-container .command { 361 | position: relative; 362 | text-align: left; 363 | margin-bottom: 5px; 364 | padding: 10px; 365 | transition: background-color 0.15s; } 366 | .options-page .commands-container .command small { 367 | opacity: 0.5; 368 | margin-left: 3px; } 369 | .options-page .commands-container .command .toggle { 370 | text-transform: capitalize; 371 | display: block; 372 | position: absolute; 373 | right: 10px; 374 | top: 5px; 375 | font-size: 14px; 376 | color: #ff9900; 377 | cursor: pointer; 378 | opacity: 0; 379 | transition: opacity 0.15s; } 380 | .options-page .commands-container .command .toggle:hover { 381 | text-decoration: underline; } 382 | .options-page .commands-container .command:hover { 383 | background-color: rgba(255, 255, 255, 0.05); } 384 | .options-page .commands-container .command:hover .toggle { 385 | opacity: 0.7; } 386 | .options-page .credits small { 387 | font-size: 14px; } 388 | .options-page .spinner { 389 | position: fixed; 390 | bottom: 5px; 391 | right: 15px; 392 | width: 30px; 393 | height: 40px; 394 | opacity: 0; 395 | background: url("chrome-extension://__MSG_@@extension_id__/images/spinner.png") repeat-x; 396 | background-size: auto 100%; 397 | transition: opacity 0.5s; 398 | -webkit-animation: _bt-spinner 0.5s infinite linear both; } 399 | .options-page.loading .spinner { 400 | opacity: 0.5; 401 | transition: opacity 0.5s 0.2s; } 402 | 403 | #__backtick__ { 404 | pointer-events: none; 405 | background: transparent !important; } 406 | #__backtick__.open { 407 | pointer-events: all; } 408 | #__backtick__:focus { 409 | outline: none; } 410 | --------------------------------------------------------------------------------