├── .gitattributes ├── .gitignore ├── screenshot.png ├── images ├── icons │ ├── tray.png │ ├── tray@2x.png │ └── tray@3x.png ├── star.svg ├── search.svg └── npm.svg ├── .editorconfig ├── readme.md ├── index.html ├── license ├── package.json ├── index.css ├── browser.js └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/screenshot.png -------------------------------------------------------------------------------- /images/icons/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray.png -------------------------------------------------------------------------------- /images/icons/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray@2x.png -------------------------------------------------------------------------------- /images/icons/tray@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray@3x.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # npm-search 2 | 3 | > Search npm via node-modules.com 4 | 5 | ![](screenshot.png) 6 | 7 | 8 | ## Dev 9 | 10 | ``` 11 | $ npm install 12 | ``` 13 | 14 | ### Run 15 | 16 | ``` 17 | $ npm start 18 | ``` 19 | 20 | ### Build 21 | 22 | ``` 23 | $ npm run build 24 | ``` 25 | 26 | Builds the app for OS X, Linux, and Windows, using [electron-packager](https://github.com/electron-userland/electron-packager). 27 | 28 | 29 | ## License 30 | 31 | MIT © [vdemedes](https://github.com/vdemedes) 32 | -------------------------------------------------------------------------------- /images/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron boilerplate 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) vdemedes (https://github.com/vdemedes) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-search", 3 | "productName": "NpmSearch", 4 | "version": "1.0.0", 5 | "description": "My excellent app", 6 | "license": "MIT", 7 | "repository": "vdemedes/npm-search", 8 | "author": { 9 | "name": "vdemedes", 10 | "email": "vdemedes@gmail.com", 11 | "url": "github.com/vdemedes" 12 | }, 13 | "scripts": { 14 | "test": "xo", 15 | "start": "electron .", 16 | "postinstall": "install-app-deps", 17 | "pack": "build --dir", 18 | "build": "build" 19 | }, 20 | "build": { 21 | "appId": "com.vdemedes.npm-search", 22 | "app-category-type": "public.app-category.developer-tools" 23 | }, 24 | "files": [ 25 | "index.js", 26 | "index.html", 27 | "index.css", 28 | "images" 29 | ], 30 | "keywords": [ 31 | "electron-app", 32 | "electron" 33 | ], 34 | "dependencies": { 35 | "auto-launch": "^2.0.1", 36 | "electron-debug": "^1.0.0", 37 | "electron-positioner": "^3.0.0", 38 | "titlebar": "^1.4.0" 39 | }, 40 | "devDependencies": { 41 | "devtron": "^1.1.0", 42 | "electron-builder": "^5.6.2", 43 | "electron-prebuilt": "^1.0.1", 44 | "eslint-config-vdemedes": "^1.0.2", 45 | "xo": "^0.15.1" 46 | }, 47 | "xo": { 48 | "extends": "vdemedes", 49 | "esnext": true, 50 | "envs": [ 51 | "node", 52 | "browser" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /images/npm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | font-family: -apple-system, 'Helvetica Neue', Helvetica, sans-serif; 13 | padding-top: 24px; 14 | } 15 | 16 | h3 { 17 | margin: 0; 18 | } 19 | 20 | .fixed-container { 21 | position: fixed; 22 | top: 0; 23 | width: 100%; 24 | background: #fff; 25 | } 26 | 27 | .titlebar { 28 | background-color: transparent; 29 | position: absolute; 30 | top: 0; 31 | width: 100%; 32 | } 33 | 34 | .titlebar-minimize, 35 | .titlebar-fullscreen { 36 | display: none; 37 | } 38 | 39 | .titlebar-close svg { 40 | margin-top: 1px; 41 | margin-left: 1px; 42 | } 43 | 44 | .title { 45 | position: absolute; 46 | text-align: center; 47 | width: 38px; 48 | left: calc(50% - 19px); 49 | top: 4px; 50 | } 51 | 52 | .npm-logo { 53 | width: 38px; 54 | } 55 | 56 | .search-form { 57 | padding: 0 8px 8px; 58 | margin-top: 24px; 59 | } 60 | 61 | .container { 62 | margin-top: 44px; 63 | padding: 0 8px 8px; 64 | display: flex; 65 | flex-direction: column; 66 | height: 332px; 67 | overflow: scroll; 68 | } 69 | 70 | .no-results { 71 | flex: 1 1 auto; 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | } 76 | 77 | .search-input { 78 | background: url('images/search.svg') no-repeat center right 8px; 79 | background-size: 16px; 80 | border-radius: 3px; 81 | border: 1px solid #ddd; 82 | width: 100%; 83 | padding: 8px 32px 8px 8px; 84 | font-size: 16px; 85 | outline: none; 86 | } 87 | 88 | .list-item { 89 | border-radius: 3px; 90 | padding: 0 10px 8px; 91 | margin-bottom: 8px; 92 | } 93 | 94 | .list-item:hover { 95 | background: #eee; 96 | } 97 | 98 | .list-item:active { 99 | background: #ddd; 100 | } 101 | 102 | .list-item-header { 103 | display: flex; 104 | justify-content: space-between; 105 | align-items: center; 106 | } 107 | 108 | .list-item-name { 109 | color: #444F5A; 110 | font-weight: 500; 111 | } 112 | 113 | .list-item-description { 114 | margin: 4px 0; 115 | font-size: 12px; 116 | color: #ccc; 117 | font-weight: 300; 118 | } 119 | 120 | .list-item-stars { 121 | color: #ccc; 122 | font-weight: 300; 123 | } 124 | 125 | .star-icon { 126 | width: 12px; 127 | margin-right: 4px; 128 | } 129 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const titlebar = require('titlebar'); 3 | const ipc = require('electron').ipcRenderer; 4 | 5 | const titleBar = titlebar(); 6 | titleBar.appendTo(document.querySelector('.js-header')); 7 | 8 | const closeButton = document.querySelector('.titlebar-close'); 9 | closeButton.addEventListener('click', () => { 10 | ipc.send('close'); 11 | }); 12 | 13 | const searchInput = document.querySelector('.js-search-input'); 14 | const searchForm = document.querySelector('.js-search-form'); 15 | searchForm.addEventListener('submit', e => { 16 | e.preventDefault(); 17 | 18 | const query = searchInput.value; 19 | search(query) 20 | .then(modules => { 21 | if (modules.length > 0) { 22 | renderModules(modules); 23 | } else { 24 | renderEmptyResult(); 25 | } 26 | 27 | ipc.send('resize'); 28 | }); 29 | }); 30 | 31 | function search (query) { 32 | return fetch('http://node-modules.com/search.json?q=' + query) 33 | .then(res => res.json()); 34 | } 35 | 36 | const container = document.querySelector('.js-container'); 37 | container.addEventListener('click', e => { 38 | let target = e.target; 39 | 40 | if (!target.matches('.js-list-item')) { 41 | target = target.closest('.js-list-item'); 42 | } 43 | 44 | if (!target) { 45 | return; 46 | } 47 | 48 | let url = target.dataset.url; 49 | 50 | if (e.altKey) { 51 | url = 'https://npmjs.org/package/' + target.dataset.name; 52 | } 53 | 54 | ipc.send('open', url, target.dataset.name); 55 | }); 56 | 57 | function renderEmptyResult () { 58 | container.innerHTML = ''; 59 | 60 | const wrapper = document.createElement('div'); 61 | wrapper.className = 'no-results'; 62 | 63 | const label = document.createElement('h3'); 64 | label.textContent = 'No modules found.'; 65 | 66 | wrapper.appendChild(label); 67 | container.appendChild(wrapper); 68 | } 69 | 70 | function renderModules (modules) { 71 | container.innerHTML = ''; 72 | 73 | const fragment = document.createDocumentFragment(); 74 | 75 | modules.forEach(module => { 76 | const listItem = document.createElement('div'); 77 | listItem.className = 'list-item js-list-item'; 78 | listItem.dataset.url = module.url; 79 | listItem.dataset.name = module.name; 80 | 81 | const header = document.createElement('header'); 82 | header.className = 'list-item-header'; 83 | 84 | const name = document.createElement('h3'); 85 | name.className = 'list-item-name'; 86 | name.textContent = module.name; 87 | 88 | const stars = document.createElement('span'); 89 | stars.className = 'list-item-stars'; 90 | stars.textContent = module.stars; 91 | 92 | const starIcon = document.createElement('img'); 93 | starIcon.className = 'star-icon'; 94 | starIcon.src = 'images/star.svg'; 95 | 96 | const description = document.createElement('p'); 97 | description.className = 'list-item-description'; 98 | description.textContent = module.description; 99 | 100 | stars.insertBefore(starIcon, stars.firstChild); 101 | 102 | header.appendChild(name); 103 | header.appendChild(stars); 104 | 105 | listItem.appendChild(header); 106 | listItem.appendChild(description); 107 | 108 | fragment.appendChild(listItem); 109 | }); 110 | 111 | container.appendChild(fragment); 112 | } 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Positioner = require('electron-positioner'); 3 | const AutoLaunch = require('auto-launch'); 4 | const electron = require('electron'); 5 | const shell = electron.shell; 6 | const ipc = electron.ipcMain; 7 | const app = electron.app; 8 | 9 | app.dock.hide(); 10 | 11 | // launch at login 12 | const appLauncher = new AutoLaunch({ 13 | name: 'npm-search' 14 | }); 15 | 16 | // adds debug features like hotkeys for triggering dev tools and reload 17 | require('electron-debug')(); 18 | 19 | // prevent window and tray from being garbage collected 20 | let mainWindow; 21 | let tray; 22 | let trayMenu; 23 | 24 | function createMainWindow() { 25 | const win = new electron.BrowserWindow({ 26 | width: 320, 27 | height: 68, 28 | frame: false, 29 | closeable: false, 30 | resizable: false 31 | }); 32 | 33 | const positioner = new Positioner(win); 34 | positioner.move('center'); 35 | 36 | win.loadURL(`file://${__dirname}/index.html`); 37 | win.on('close', e => { 38 | e.preventDefault(); 39 | closeMainWindow(); 40 | }); 41 | 42 | return win; 43 | } 44 | 45 | function closeMainWindow () { 46 | mainWindow.hide(); 47 | mainWindow.setSize(320, 68, false); 48 | mainWindow.reload(); 49 | } 50 | 51 | function createTray () { 52 | const tray = new electron.Tray(`${__dirname}/images/icons/tray.png`); 53 | tray.setToolTip('Search npm'); 54 | 55 | tray.on('click', () => { 56 | mainWindow.show(); 57 | mainWindow.focus(); 58 | }); 59 | 60 | tray.on('right-click', () => { 61 | tray.popUpContextMenu(trayMenu); 62 | }); 63 | 64 | return tray; 65 | } 66 | 67 | // list of last opened modules 68 | const modules = []; 69 | 70 | // handler for last opened module menu item click event 71 | function onModuleClick (menuItem) { 72 | const module = modules.find(module => module.name === menuItem.label); 73 | shell.openExternal(module.url); 74 | } 75 | 76 | function createTrayMenu () { 77 | const lastOpened = new electron.MenuItem({ 78 | label: 'Last opened', 79 | enabled: false 80 | }); 81 | 82 | const preferences = new electron.MenuItem({ 83 | label: 'Preferences', 84 | enabled: false 85 | }); 86 | 87 | const openAtLogin = new electron.MenuItem({ 88 | label: 'Open at login', 89 | type: 'checkbox', 90 | click: menuItem => { 91 | appLauncher.isEnabled() 92 | .then(isEnabled => { 93 | menuItem.checked = !isEnabled; 94 | 95 | if (isEnabled) { 96 | appLauncher.disable(); 97 | } else { 98 | appLauncher.enable(); 99 | } 100 | }); 101 | } 102 | }); 103 | 104 | const quit = new electron.MenuItem({ 105 | label: process.platform === 'darwin' ? `Quit ${app.getName()}` : 'Exit', 106 | click() { 107 | app.exit(0); 108 | } 109 | }); 110 | 111 | const separator = new electron.MenuItem({ 112 | type: 'separator' 113 | }); 114 | 115 | const moduleItems = modules.map(module => { 116 | return new electron.MenuItem({ 117 | label: module.name, 118 | click: onModuleClick 119 | }); 120 | }); 121 | 122 | const menu = new electron.Menu(); 123 | menu.append(lastOpened); 124 | 125 | moduleItems.forEach(moduleItem => { 126 | menu.append(moduleItem); 127 | }); 128 | 129 | menu.append(separator); 130 | menu.append(preferences); 131 | menu.append(openAtLogin); 132 | menu.append(separator); 133 | menu.append(quit); 134 | 135 | appLauncher.isEnabled() 136 | .then(isEnabled => { 137 | openAtLogin.checked = isEnabled; 138 | }); 139 | 140 | return menu; 141 | } 142 | 143 | ipc.on('resize', () => { 144 | mainWindow.setSize(320, 400, true); 145 | }); 146 | 147 | ipc.on('close', () => { 148 | closeMainWindow(); 149 | }); 150 | 151 | ipc.on('open', (e, url, name) => { 152 | if (modules.length === 5) { 153 | modules.pop(); 154 | } 155 | 156 | modules.unshift({ url, name }); 157 | 158 | shell.openExternal(url); 159 | closeMainWindow(); 160 | 161 | trayMenu = createTrayMenu(); 162 | }); 163 | 164 | app.on('ready', () => { 165 | mainWindow = createMainWindow(); 166 | tray = createTray(); 167 | trayMenu = createTrayMenu(); 168 | }); 169 | --------------------------------------------------------------------------------