├── .gitignore ├── .nvmrc ├── .storybook ├── .babelrc ├── addons.js ├── config.js ├── default-storybook-webpack-config.js ├── presets.js ├── preview-head.html └── webpack.config.js ├── README.md ├── add.html ├── es-dev-server.config.js ├── index.html ├── netlify.toml ├── package.json ├── packages ├── app │ ├── src │ │ ├── owc-cat-app.css.js │ │ └── owc-cat-app.js │ └── stories │ │ ├── index.mdx │ │ └── index.stories.js ├── filters │ ├── src │ │ └── owc-cat-filters.js │ └── stories │ │ ├── index.mdx │ │ └── index.stories.js ├── header │ ├── src │ │ ├── open-wc-logo.svg.js │ │ └── owc-cat-header.js │ └── stories │ │ ├── index.mdx │ │ └── index.stories.js ├── intro │ ├── src │ │ ├── owc-button.css.js │ │ └── owc-cat-intro.js │ └── stories │ │ ├── index.mdx │ │ └── index.stories.js └── item │ ├── src │ ├── github-markdown.css.js │ ├── logos │ │ ├── logo-github.js │ │ ├── logo-npm.js │ │ └── logo-unpkg.js │ ├── owc-cat-item.css.js │ ├── owc-cat-item.js │ ├── owc-tabs.js │ └── remarkable-markdown.js │ └── stories │ ├── index.mdx │ └── index.stories.js ├── research ├── elasticIndex.json ├── faunadb.txt └── normalized │ ├── db.js │ └── schema.gql ├── rollup.config.js ├── schema.gql ├── src └── lambda │ ├── check-package-with-version.js │ ├── helpers │ ├── db.js │ ├── elastic.js │ ├── helpers.js │ └── npm.js │ └── search.js ├── todo.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | # code coverage folders 9 | coverage/ 10 | 11 | ## npm 12 | node_modules 13 | npm-debug.log 14 | 15 | ## lerna 16 | lerna-debug.log 17 | 18 | ## temp folders 19 | /.tmp/ 20 | 21 | # build hp 22 | /_site/ 23 | 24 | ## build output 25 | /dist 26 | /build-stats.json 27 | /lambda 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-object-rest-spread"], 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry", 8 | "corejs": "2" 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-storysource/register'; 3 | import '@storybook/addon-knobs/register'; 4 | import '@storybook/addon-backgrounds/register'; 5 | import '@storybook/addon-a11y/register'; 6 | import '@storybook/addon-links/register'; 7 | import '@storybook/addon-viewport/register'; 8 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@storybook/polymer'; 2 | import { withA11y } from '@storybook/addon-a11y'; 3 | import '@storybook/addon-console'; 4 | 5 | // const req = require.context('../stories', true, /\.stories\.js$/); 6 | // function loadStories() { 7 | // req.keys().forEach(filename => req(filename)); 8 | // } 9 | 10 | addDecorator(withA11y); 11 | // configure(loadStories, module); 12 | 13 | // import { configure } from '@storybook/polymer'; 14 | 15 | const req = require.context('../packages', true, /\.stories\.(js|mdx)$/); 16 | configure(req, module); 17 | 18 | // force full reload to not reregister web components 19 | if (module.hot) { 20 | module.hot.accept(req.id, () => { 21 | const currentLocationHref = window.location.href; 22 | window.history.pushState(null, null, currentLocationHref); 23 | location.reload(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.storybook/default-storybook-webpack-config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, transpilePackages = ['lit-html', 'lit-element', '@open-wc'] }) => { 2 | config.module.rules.push({ 3 | test: [/\.stories\.js$/, /index\.js$/], 4 | loaders: [require.resolve('@storybook/source-loader')], 5 | enforce: 'pre', 6 | }); 7 | 8 | // this is a separate config for only those packages 9 | // the main storybook will use the .babelrc which is needed so storybook itself works in IE 10 | config.module.rules.push({ 11 | test: new RegExp(`node_modules(\\/|\\\\)(${transpilePackages.join('|')})(.*)\\.js$`), 12 | use: { 13 | loader: 'babel-loader', 14 | options: { 15 | plugins: [ 16 | '@babel/plugin-syntax-dynamic-import', 17 | '@babel/plugin-proposal-object-rest-spread', 18 | ], 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | useBuiltIns: 'entry', 24 | corejs: '2', 25 | }, 26 | ], 27 | ], 28 | babelrc: false, 29 | }, 30 | }, 31 | }); 32 | 33 | return config; 34 | }; 35 | -------------------------------------------------------------------------------- /.storybook/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = ['@storybook/addon-docs/html/preset']; 2 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('./default-storybook-webpack-config.js'); 2 | 3 | module.exports = ({ config }) => { 4 | return defaultConfig({ config, transpilePackages: ['lit-html', 'lit-element', '@open-wc'] }); 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ## 🛠 Status: In Development 2 | > 3 | > This custom elements catalog is currently in development. 4 | 5 |

6 | 7 |

8 | 9 | ## Custom Elements Catalog 10 | 11 | [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](https://github.com/open-wc) 12 | 13 | This let's you search the npm registry for web components. 14 | 15 | ## Adding a web component 16 | 17 | Be sure to have a `custom-elements.json` describing all (or one) web components within your package. 18 | 19 | Example: 20 | 21 | ```json 22 | { 23 | "version": 2, 24 | "tags": [ 25 | { 26 | "label": "test-wc-card" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | As this is still a pilot phase you will have to manually index your package. 33 | 34 | 1. Go to [http://catalog.open-wc.org/add.html](http://catalog.open-wc.org/add.html) 35 | 2. Enter you package name followed by `@` and version (example `test-wc-card@0.0.6` or `@foo/bar@1.0.0`) 36 | 37 | (only the latest npm versions get added to the search index - but older vesions can still be added to our database) 38 | 39 | ## Working on it 40 | 41 | ```bash 42 | yarn install 43 | yarn storybook # for individual parts with mocked data 44 | yarn start # for live page 45 | ``` 46 | 47 | ## Background 48 | 49 | Uses 50 | 51 | - Funadb to store the data in a normalized way 52 | - Elasticsearch (aws) to store flattened docs to make them performant searchable 53 | 54 | ## custom-elements.json 55 | 56 | This is still a proposal so [follow the discussion!!](https://github.com/w3c/webcomponents/issues/776). 57 | 58 | Possible example: 59 | 60 | ```json 61 | { 62 | "version": 2, 63 | "tags": [ 64 | { 65 | "label": "test-wc-card", 66 | "attributes": [ 67 | { 68 | "label": "header" 69 | }, 70 | { 71 | "label": "side", 72 | "values": [{ "label": "A" }, { "label": "B" }] 73 | } 74 | ], 75 | "properties": [ 76 | { 77 | "label": "header" 78 | }, 79 | { 80 | "label": "headerColor" 81 | }, 82 | { 83 | "label": "side", 84 | "values": [{ "label": "A" }, { "label": "B" }] 85 | } 86 | ], 87 | "events": [], 88 | "slots": [] 89 | } 90 | ] 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 23 | Web Components Catalog 24 | 25 | 26 |
27 | 28 | 29 |

30 | You will need to have a custom-element.json see 31 | help. 32 |

33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /es-dev-server.config.js: -------------------------------------------------------------------------------- 1 | const proxy = require('koa-proxies'); 2 | 3 | module.exports = { 4 | port: 8080, 5 | middlewares: [ 6 | proxy('/.netlify/functions/', { 7 | target: 'http://localhost:9000', 8 | rewrite: path => path.replace(/^\/\\.netlify\/functions/, ''), 9 | }), 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 23 | Web Components Catalog 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "lambda" 3 | publish = "dist" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@open-wc/catalog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "open-wc", 6 | "homepage": "https://github.com/open-wc/open-wc/", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/open-wc/open-wc.git", 11 | "directory": "packages/generator-open-wc" 12 | }, 13 | "private": true, 14 | "scripts": { 15 | "start": "run-p start:app start:lambda", 16 | "start:app": "es-dev-server --app-index index.html --node-resolve --watch --open", 17 | "start:lambda": "netlify-lambda serve src/lambda", 18 | "build": "npm run build:lambda && npm run build:app && cp ./add.html ./dist/add.html && npm run build:storybook", 19 | "build:lambda": "netlify-lambda build src/lambda", 20 | "build:app": "rimraf dist && rollup -c rollup.config.js", 21 | "lint": "npm run lint:eslint && npm run lint:prettier", 22 | "lint:eslint": "eslint --ext .js,.html . --ignore-path .gitignore", 23 | "lint:prettier": "prettier \"**/*.js\" --check --ignore-path .gitignore", 24 | "format": "npm run format:eslint && npm run format:prettier", 25 | "format:eslint": "eslint --ext .js,.html . --fix --ignore-path .gitignore", 26 | "format:prettier": "prettier \"**/*.js\" --write --ignore-path .gitignore", 27 | "start:build": "cd dist && es-dev-server --open", 28 | "storybook": "start-storybook -p 9001", 29 | "build:storybook": "build-storybook -o ./dist/storybook" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.3.3", 33 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 34 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 35 | "@babel/preset-env": "^7.0.0", 36 | "@open-wc/building-rollup": "^0.9.0", 37 | "@open-wc/eslint-config": "^1.0.0", 38 | "@open-wc/prettier-config": "^0.1.10", 39 | "@storybook/addon-a11y": "~5.2.0", 40 | "@storybook/addon-actions": "~5.2.0", 41 | "@storybook/addon-backgrounds": "~5.2.0", 42 | "@storybook/addon-console": "^1.1.0", 43 | "@storybook/addon-docs": "^5.2.1", 44 | "@storybook/addon-knobs": "~5.2.0", 45 | "@storybook/addon-links": "~5.2.0", 46 | "@storybook/addon-notes": "~5.2.0", 47 | "@storybook/addon-storysource": "~5.2.0", 48 | "@storybook/addon-viewport": "~5.2.0", 49 | "@storybook/polymer": "~5.2.0", 50 | "@storybook/source-loader": "~5.2.0", 51 | "@types/storybook__addon-actions": "^3.4.1", 52 | "@types/storybook__addon-backgrounds": "^3.2.1", 53 | "@types/storybook__addon-links": "^3.3.3", 54 | "@types/storybook__addon-notes": "^3.3.3", 55 | "@webcomponents/webcomponentsjs": "^2.2.0", 56 | "babel-loader": "^8.0.0", 57 | "es-dev-server": "^1.18.0", 58 | "husky": "^1.0.0", 59 | "koa-proxies": "^0.8.1", 60 | "lint-staged": "^8.0.0", 61 | "moment": "^2.0.0", 62 | "netlify-lambda": "^1.6.3", 63 | "npm-run-all": "^4.1.5", 64 | "polymer-webpack-loader": "^2.0.0", 65 | "rimraf": "^2.6.3", 66 | "rollup": "^1.15.4" 67 | }, 68 | "dependencies": { 69 | "@github/time-elements": "^3.0.7", 70 | "@storybook/html": "^5.2.1", 71 | "graphql-request": "^1.8.2", 72 | "h.js": "^4.0.0", 73 | "lit-element": "^2.0.1", 74 | "lit-html": "^1.0.0", 75 | "node-fetch": "^2.6.0", 76 | "remarkable": "^2.0.0", 77 | "semver": "^6.3.0", 78 | "wc-spinners": "^1.0.0" 79 | }, 80 | "lint-staged": { 81 | "*.js": [ 82 | "eslint --fix", 83 | "prettier --write", 84 | "git add" 85 | ] 86 | }, 87 | "eslintConfig": { 88 | "extends": [ 89 | "@open-wc/eslint-config", 90 | "eslint-config-prettier" 91 | ] 92 | }, 93 | "prettier": "@open-wc/prettier-config", 94 | "husky": { 95 | "hooks": { 96 | "pre-commit": "lint-staged" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/app/src/owc-cat-app.css.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | :host { 5 | color: #1a2b42; 6 | display: flex; 7 | flex-flow: column; 8 | min-height: 100vh; 9 | background: #eee; 10 | } 11 | 12 | a { 13 | color: #217ff9; 14 | } 15 | 16 | #content { 17 | display: flex; 18 | flex-grow: 1; 19 | flex-flow: column; 20 | margin: 0 auto; 21 | padding-top: 10px; 22 | } 23 | 24 | main { 25 | position: relative; 26 | flex-grow: 1; 27 | width: 100vw; 28 | } 29 | 30 | owc-cat-filters { 31 | z-index: 100; 32 | } 33 | 34 | .app-footer { 35 | color: #a8a8a8; 36 | font-size: calc(10px + 1vmin); 37 | text-align: center; 38 | font-size: 16px; 39 | font-weight: normal; 40 | } 41 | 42 | /* mobile details swipe/scroll */ 43 | :host([show-mobile-detail]) .items-wrapper { 44 | margin-top: 15px; 45 | } 46 | 47 | .pill { 48 | white-space: nowrap; 49 | border-radius: 10px; 50 | padding: 5px 10px; 51 | font-size: 12px; 52 | text-transform: none; 53 | margin: 10px 5px; 54 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 55 | background: #ccc; 56 | color: #000; 57 | border: 1px solid #aaa; 58 | } 59 | 60 | [owc-tabs-active].pill { 61 | background: #666; 62 | color: #fff; 63 | } 64 | 65 | @keyframes app-logo-spin { 66 | from { 67 | transform: rotate(0deg); 68 | } 69 | to { 70 | transform: rotate(360deg); 71 | } 72 | } 73 | 74 | #loading { 75 | display: none; 76 | } 77 | 78 | :host([loading]) #loading { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | position: absolute; 83 | width: 100%; 84 | height: 100%; 85 | top: 0; 86 | left: 0; 87 | background: rgba(200, 200, 200, 0.5); 88 | --semipolar-spinner__color: #999; 89 | } 90 | 91 | @media only screen and (min-width: 420px) { 92 | :host { 93 | background: #f7f7f7; 94 | } 95 | 96 | #content { 97 | flex-flow: row; 98 | padding: 20px; 99 | 100 | max-width: 1080px; 101 | } 102 | } 103 | `; 104 | -------------------------------------------------------------------------------- /packages/app/src/owc-cat-app.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | import 'wc-spinners/dist/semipolar-spinner.js'; 4 | 5 | import owcAppStyle from './owc-cat-app.css.js'; 6 | import '../../header/src/owc-cat-header.js'; 7 | import '../../intro/src/owc-cat-intro.js'; 8 | import '../../item/src/owc-cat-item.js'; 9 | import '../../filters/src/owc-cat-filters.js'; 10 | import '../../item/src/owc-tabs.js'; 11 | 12 | function isMobile() { 13 | return window.innerWidth < 600; 14 | } 15 | 16 | class OwcCatApp extends LitElement { 17 | static get properties() { 18 | return { 19 | data: { type: Array }, 20 | intro: { type: Boolean }, 21 | loading: { type: Boolean, reflect: true }, 22 | showMobileDetail: { type: Boolean, reflect: true, attribute: 'show-mobile-detail' }, 23 | detailsTabIndex: { type: Number }, 24 | }; 25 | } 26 | 27 | static get styles() { 28 | return [ 29 | owcAppStyle, 30 | css` 31 | h1 { 32 | font-family: 'Roboto Mono'; 33 | text-align: center; 34 | margin-bottom: 5px; 35 | } 36 | 37 | .not-found { 38 | padding: 60px; 39 | text-align: center; 40 | } 41 | `, 42 | ]; 43 | } 44 | 45 | constructor() { 46 | super(); 47 | this.intro = true; 48 | this.loading = false; 49 | this.showMobileDetail = false; 50 | this.detailsTabIndex = 1; 51 | this.data = []; 52 | this.query = ''; 53 | this.__firstSearch = true; 54 | 55 | this.shadowRoot.addEventListener('search', () => { 56 | this.search(); 57 | }); 58 | } 59 | 60 | connectedCallback() { 61 | super.connectedCallback(); 62 | const params = new URL(window.location.href).searchParams; 63 | if (params.get('q')) { 64 | this.search(params.get('q'), params.get('type')); 65 | } 66 | } 67 | 68 | render() { 69 | let list = ''; 70 | 71 | if (this.data.length > 0) { 72 | list = this.data.map( 73 | (item, i) => html` 74 | ${this.showMobileDetail 75 | ? html` 76 |
${item.name}
77 | ` 78 | : html``} 79 | { 83 | if (isMobile()) { 84 | const clickedItem = ev.target; 85 | this.showMobileDetail = clickedItem.showDetails; 86 | 87 | this.updateComplete.then(() => { 88 | if (this.showMobileDetail) { 89 | this.shadowRoot.querySelector('.items-wrapper').scrollIntoView(); 90 | this.shadowRoot.querySelector('owc-tabs').activeIndex = 91 | clickedItem.__catItemIndex; 92 | } else { 93 | [...this.shadowRoot.querySelectorAll('owc-cat-item')][ 94 | clickedItem.__catItemIndex 95 | ].scrollIntoView({ behavior: 'smooth' }); 96 | } 97 | }); 98 | } 99 | }} 100 | @detailsTabIndexChanged=${ev => { 101 | if (isMobile()) { 102 | this.detailsTabIndex = ev.target.detailsTabIndex; 103 | } 104 | }} 105 | .name=${item.name} 106 | .description=${item.description} 107 | .version=${item.version} 108 | .versionTime=${item.versionTime} 109 | .flattenedDependencies=${item.flattenedDependencies} 110 | .size=${item.size} 111 | .sizeGzip=${item.sizeGzip} 112 | .githubStars=${item.githubStars} 113 | .githubUrl=${item.githubUrl} 114 | .npmUrl=${item.npmUrl} 115 | .unpkgUrl=${item.unpkgUrl} 116 | .bundlephobiaUrl=${item.bundlephobiaUrl} 117 | .readme=${item.readme} 118 | .demoUrl=${item.demoUrl} 119 | .showDetails=${this.showMobileDetail} 120 | .detailsTabIndex=${this.detailsTabIndex} 121 | > 122 | 123 | `, 124 | ); 125 | } else { 126 | list = html` 127 |
128 | We could not find any web component for "${this.header ? this.header.searchValue : '...'} 129 | ". Maybe too many filters are set? 130 |
131 | In alpha phase you need to manually index packages via 132 | add.html. 133 |
134 | You can learn more how you can index a webcomponent under 135 | 136 | help 137 | 138 | . 139 |
140 | `; 141 | if (this.__firstSearch) { 142 | list = html``; 143 | } 144 | } 145 | 146 | return html` 147 | ${this.intro 148 | ? html` 149 | 150 | ` 151 | : html` 152 | 153 |
154 | 155 |
156 |
157 | 158 |
159 | 160 |
161 | ${this.showMobileDetail 162 | ? html` 163 | { 165 | if (isMobile()) { 166 | this.shadowRoot.querySelector('.items-wrapper').scrollIntoView(); 167 | } 168 | }} 169 | > 170 | ${list} 171 | 172 | ` 173 | : html` 174 | ${list} 175 | `} 176 |
177 |
178 |
179 | 180 | 186 | `} 187 | `; 188 | } 189 | 190 | get header() { 191 | return this.shadowRoot.querySelector('owc-cat-header'); 192 | } 193 | 194 | get filters() { 195 | return this.shadowRoot.querySelector('owc-cat-filters'); 196 | } 197 | 198 | _introSearch(ev) { 199 | ev.stopPropagation(); 200 | this.intro = false; 201 | const { searchValue } = this.shadowRoot.querySelector('owc-cat-intro'); 202 | this.updateComplete.then(async () => { 203 | await this.header.updateComplete; 204 | this.header.searchValue = searchValue; 205 | this.search(); 206 | }); 207 | } 208 | 209 | async search() { 210 | this.loading = true; 211 | 212 | const data = new FormData(this.filters.formEl); 213 | data.append('queryString', this.header.searchValue); 214 | const urlParams = new URLSearchParams(data).toString(); 215 | 216 | const url = `/.netlify/functions/search?${urlParams}`; 217 | const response = await fetch(url); 218 | const json = await response.json(); 219 | this.data = Array.from(json); 220 | 221 | this.loading = false; 222 | this.__firstSearch = false; 223 | } 224 | } 225 | 226 | customElements.define('owc-cat-app', OwcCatApp); 227 | -------------------------------------------------------------------------------- /packages/app/stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Preview, DocsStory } from '@storybook/addon-docs/blocks'; 2 | 3 | # Catalog App 4 | 5 | This is the main entry point. 6 | 7 | ## Start 8 | 9 | When you "boot up" you just see a the input to start searching. 10 | This is intentionally kept very minimal to make it easy to get started. 11 | 12 | 13 | 14 | 15 | 16 | ## Loading 17 | 18 | Once you enter your search entry and hit enter the spinning starts. 19 | 20 | 21 | 22 | 23 | 24 | ## Result 25 | 26 | Which will bring you to your first result. 27 | 28 | 29 | 30 | 31 | 32 | ## Retrigger Search 33 | 34 | If you retrigger the search a spinner will appear on top of it. 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/app/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | // import { forceReRender } from '@storybook/polymer'; 2 | import { html } from 'lit-html'; 3 | import mdx from './index.mdx'; 4 | 5 | import '../src/owc-cat-app.js'; 6 | 7 | // const fullReadme = '# \\\n\nThis webcomponent follows the [open-wc](https://github.com/open-wc/open-wc) recommendation.\n\n## Installation\n```bash\nnpm i test-wc-card\n```\n\n## Usage\n```html\n\n\n\n```\n\n## Testing using karma (if applied by author)\n```bash\nnpm run test\n```\n\n## Testing using karma via browserstack (if applied by author)\n```bash\nnpm run test:bs\n```\n\n## Demoing using storybook (if applied by author)\n```bash\nnpm run storybook\n```\n\n## Linting (if applied by author)\n```bash\nnpm run lint\n```'; 8 | 9 | export default { 10 | title: 'App', 11 | parameters: { 12 | docs: { 13 | page: mdx, 14 | }, 15 | }, 16 | }; 17 | 18 | export const standard = () => 19 | html` 20 | 21 | `; 22 | 23 | export const loadingFirstTime = () => 24 | html` 25 | 26 | `; 27 | 28 | export const loading = () => 29 | html` 30 | 53 | `; 54 | 55 | export const singleResult = () => 56 | html` 57 | 79 | `; 80 | 81 | export const twoResults = () => 82 | html` 83 | 137 | `; 138 | -------------------------------------------------------------------------------- /packages/filters/src/owc-cat-filters.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | class OwcCatFilters extends LitElement { 4 | static get styles() { 5 | return [ 6 | css` 7 | label { 8 | display: block; 9 | } 10 | 11 | h3 { 12 | text-align: left; 13 | position: relative; 14 | padding: 7px 7px 7px 15px; 15 | margin: 0 10px; 16 | background: #fff; 17 | border: 1px solid #ccc; 18 | } 19 | 20 | h3::after { 21 | content: '∨'; 22 | display: block; 23 | position: absolute; 24 | right: 15px; 25 | top: 2px; 26 | font-weight: normal; 27 | font-size: 24px; 28 | } 29 | 30 | #content { 31 | display: none; 32 | box-sizing: border-box; 33 | position: absolute; 34 | left: 0; 35 | top: 0; 36 | width: 100%; 37 | height: 100%; 38 | background: #fff; 39 | padding: 10px; 40 | text-align: center; 41 | } 42 | 43 | #content h3 button { 44 | float: right; 45 | } 46 | 47 | :host([opened]) #content { 48 | display: block; 49 | } 50 | 51 | :host([opened]) h3::after { 52 | transform: rotate(180deg); 53 | top: 6px; 54 | } 55 | 56 | @media only screen and (min-width: 420px) { 57 | :host { 58 | margin-right: 20px; 59 | } 60 | 61 | h1 { 62 | font-size: 30px; 63 | } 64 | 65 | h3 { 66 | text-align: left; 67 | padding: 0; 68 | border: none; 69 | margin: 0; 70 | background: none; 71 | } 72 | 73 | h3::after { 74 | display: none; 75 | } 76 | 77 | .mobile { 78 | display: none; 79 | } 80 | 81 | #content { 82 | display: block; 83 | box-sizing: border-box; 84 | position: static; 85 | width: auto; 86 | height: auto; 87 | padding: 0; 88 | text-align: left; 89 | background: none; 90 | } 91 | } 92 | `, 93 | ]; 94 | } 95 | 96 | static get properties() { 97 | return { 98 | opened: { type: Boolean, reflect: true }, 99 | }; 100 | } 101 | 102 | toggle() { 103 | this.opened = !this.opened; 104 | } 105 | 106 | constructor() { 107 | super(); 108 | this.addEventListener('submit', ev => { 109 | ev.preventDefault(); 110 | }); 111 | } 112 | 113 | firstUpdated() { 114 | super.firstUpdated(); 115 | this.shadowRoot.addEventListener('change', () => { 116 | this._onSubmit(); 117 | }); 118 | } 119 | 120 | render() { 121 | return html` 122 |
123 |

Filters

124 |
125 |

126 | Filters 127 |

128 |

Dependencies

129 |
130 | 133 | 136 | 139 |
142 | all below is not
143 | yet implemented 🙈 144 |
145 | 146 | 147 |
148 |

Last Release

149 |
150 | 151 | 152 | 153 |
154 |

Size

155 |
156 | 157 | 161 | 165 |
166 |

Properties

167 |
168 | 169 | 170 |
171 | 172 |

Github

173 |
174 | 175 | 176 |
177 |
178 |
179 | `; 180 | } 181 | 182 | get formEl() { 183 | return this.shadowRoot.getElementById('form'); 184 | } 185 | 186 | _onSubmit(ev) { 187 | if (ev) { 188 | ev.preventDefault(); 189 | } 190 | this.dispatchEvent(new Event('search', { bubbles: true })); 191 | } 192 | } 193 | 194 | customElements.define('owc-cat-filters', OwcCatFilters); 195 | -------------------------------------------------------------------------------- /packages/filters/stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Preview, DocsStory } from '@storybook/addon-docs/blocks'; 2 | 3 | # Filters 4 | 5 | The filters allow you narrow down your results to selection what is most important to your use-case. 6 | 7 | The most prominent filters are for dependencies. 8 | 9 | ## Standard Usage 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/filters/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | // import { forceReRender } from '@storybook/polymer'; 2 | import { html } from 'lit-html'; 3 | import mdx from './index.mdx'; 4 | 5 | import '../src/owc-cat-filters.js'; 6 | 7 | export default { 8 | title: 'Filters', 9 | parameters: { 10 | docs: { 11 | page: mdx, 12 | }, 13 | }, 14 | }; 15 | 16 | export const standard = () => 17 | html` 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /packages/header/src/open-wc-logo.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | 3 | export default html` 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | `; 26 | -------------------------------------------------------------------------------- /packages/header/src/owc-cat-header.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | import openWcLogo from './open-wc-logo.svg.js'; 4 | 5 | class OwcCatHeader extends LitElement { 6 | static get styles() { 7 | return [ 8 | css` 9 | :host { 10 | display: block; 11 | padding: 10px; 12 | background: linear-gradient(to bottom, #8e9eab, #eef2f3); 13 | } 14 | 15 | h1 { 16 | display: none; 17 | } 18 | 19 | #content { 20 | margin: 0 auto; 21 | display: flex; 22 | justify-content: center; 23 | max-width: 700px; 24 | } 25 | 26 | .app-header__logo { 27 | margin-right: 10px; 28 | } 29 | 30 | svg { 31 | width: 50px; 32 | animation: app-logo-spin infinite 20s linear; 33 | } 34 | 35 | .input-wrapper { 36 | display: flex; 37 | width: 100%; 38 | } 39 | 40 | input { 41 | outline: none; 42 | border: none; 43 | } 44 | 45 | input { 46 | width: 100%; 47 | height: 20px; 48 | padding: 16px 24px; 49 | border-radius: 28px; 50 | font-size: 16px; 51 | /* TODO: transition box-shadow is slow, use pseudo elements with opacity instead */ 52 | transition: box-shadow 300ms; 53 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 54 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 55 | } 56 | 57 | input::placeholder { 58 | color: #9e9e9e; 59 | font-size: 16px; 60 | } 61 | 62 | input:focus { 63 | box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 64 | 0 3px 5px -1px rgba(0, 0, 0, 0.4); 65 | } 66 | 67 | button[type='submit'] { 68 | margin-right: 10px; 69 | height: 34px; 70 | margin-top: 8px; 71 | } 72 | 73 | @media only screen and (min-width: 640px) { 74 | :host { 75 | padding: 20px; 76 | } 77 | 78 | h1 { 79 | display: block; 80 | margin-top: 0; 81 | text-align: center; 82 | color: #fff; 83 | text-shadow: #333 3px 3px 2px; 84 | } 85 | 86 | .app-header__logo { 87 | margin-right: 20px; 88 | } 89 | } 90 | `, 91 | ]; 92 | } 93 | 94 | render() { 95 | return html` 96 |

Web Component Catalog

97 |
98 |
99 | 102 |
103 | 104 |
105 |
106 |
107 | `; 108 | } 109 | 110 | get searchValue() { 111 | return this.shadowRoot.getElementById('searchInput').value; 112 | } 113 | 114 | set searchValue(value) { 115 | this.shadowRoot.getElementById('searchInput').value = value; 116 | } 117 | 118 | _onSubmit(ev) { 119 | ev.preventDefault(); 120 | this.dispatchEvent(new Event('search', { bubbles: true })); 121 | } 122 | } 123 | 124 | customElements.define('owc-cat-header', OwcCatHeader); 125 | -------------------------------------------------------------------------------- /packages/header/stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Preview, DocsStory } from '@storybook/addon-docs/blocks'; 2 | 3 | # Header 4 | 5 | The header is similar to the intro but it's placed at the top and behaviors differently on mobile. 6 | 7 | ## Standard 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/header/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | // import { forceReRender } from '@storybook/polymer'; 2 | import { html } from 'lit-html'; 3 | import mdx from './index.mdx'; 4 | 5 | import '../src/owc-cat-header.js'; 6 | 7 | export default { 8 | title: 'Header', 9 | parameters: { 10 | docs: { 11 | page: mdx, 12 | }, 13 | }, 14 | }; 15 | 16 | export const standard = () => 17 | html` 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /packages/intro/src/owc-button.css.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | /* reset */ 5 | .owc-button { 6 | font: inherit; 7 | font-size: 16px; 8 | margin: 0; 9 | border: 0; 10 | padding: 0; 11 | color: inherit; 12 | background-color: transparent; 13 | text-align: left; 14 | white-space: normal; 15 | overflow: visible; 16 | 17 | user-select: none; 18 | -moz-user-select: none; 19 | -webkit-user-select: none; 20 | -ms-user-select: none; 21 | } 22 | 23 | .owc-button { 24 | border-radius: 4px; 25 | padding: 8px; 26 | border: 1px solid #0077ff; 27 | display: block; 28 | text-decoration: none; 29 | cursor: pointer; 30 | } 31 | 32 | .owc-button-filled { 33 | background-color: #0077ff; 34 | color: white; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /packages/intro/src/owc-cat-intro.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | import owcButtonStyles from './owc-button.css.js'; 4 | 5 | class OwcCatIntro extends LitElement { 6 | static get styles() { 7 | return [ 8 | owcButtonStyles, 9 | css` 10 | :host { 11 | display: flex; 12 | height: 100vh; 13 | background: linear-gradient(to bottom, #8e9eab, #eef2f3); 14 | flex-flow: column; 15 | justify-content: center; 16 | } 17 | 18 | h1 { 19 | font-size: 24px; 20 | text-align: center; 21 | color: #fff; 22 | text-shadow: #333 3px 3px 2px; 23 | } 24 | 25 | #content { 26 | width: 90%; 27 | max-width: 500px; 28 | margin: 0 auto; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | flex-flow: column; 33 | } 34 | 35 | .input-wrapper { 36 | display: flex; 37 | width: 100%; 38 | } 39 | 40 | input { 41 | outline: none; 42 | border: none; 43 | } 44 | 45 | input { 46 | width: 100%; 47 | height: 20px; 48 | padding: 16px 24px; 49 | border-radius: 28px; 50 | font-size: 16px; 51 | /* TODO: transition box-shadow is slow, use pseudo elements with opacity instead */ 52 | transition: box-shadow 300ms; 53 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 54 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 55 | } 56 | 57 | input::placeholder { 58 | color: #9e9e9e; 59 | font-size: 16px; 60 | } 61 | 62 | input:focus { 63 | box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 64 | 0 3px 5px -1px rgba(0, 0, 0, 0.4); 65 | } 66 | 67 | button[type='submit'] { 68 | margin-top: 12px; 69 | max-width: 120px; 70 | height: 34px; 71 | text-align: center; 72 | } 73 | 74 | @media only screen and (min-width: 420px) { 75 | h1 { 76 | font-size: 30px; 77 | } 78 | } 79 | `, 80 | ]; 81 | } 82 | 83 | render() { 84 | return html` 85 |

Web Component Catalog

86 |
87 |
88 |
89 | 95 |
96 | 99 |

100 | In alpha phase you need to manually index packages via add.html 101 |

102 |
103 |
104 | `; 105 | } 106 | 107 | /** 108 | * The current filled in value of the search input 109 | * 110 | * @example getting search value 111 | * // user writes "button" into the input field 112 | * expect(el.searchValue).to.equal('button'); 113 | * 114 | * @returns {string} 115 | */ 116 | get searchValue() { 117 | return this.shadowRoot.getElementById('searchInput').value; 118 | } 119 | 120 | /** 121 | * @example search value in templates 122 | * 123 | * // users sees "button" filled in the search input 124 | * 125 | * @example setting search value 126 | * el.searchValue = 'button'; 127 | * // users sees "button" filled in the search input 128 | * 129 | * @param {string} value Value to be set for the input 130 | */ 131 | set searchValue(value) { 132 | this.shadowRoot.getElementById('searchInput').value = value; 133 | } 134 | 135 | _onSubmit(ev) { 136 | ev.preventDefault(); 137 | /** 138 | * Fires when the user wishes to search 139 | * 140 | * @example 141 | * 142 | * 143 | * @example 144 | * el.addEventListener('search', this.yourSearchImplemntation.bind(this)); 145 | */ 146 | this.dispatchEvent(new Event('search', { bubbles: true })); 147 | } 148 | } 149 | 150 | customElements.define('owc-cat-intro', OwcCatIntro); 151 | -------------------------------------------------------------------------------- /packages/intro/stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Preview, DocsStory } from '@storybook/addon-docs/blocks'; 2 | 3 | # Intro 4 | 5 | The intro is a full view component which displays a single search input. 6 | On submit it fires a search event. 7 | 8 | ## Standard Usage 9 | 10 | 11 | 12 | 13 | 14 | ## Properties 15 | 16 | | Name | Type | Description | 17 | | ----------- | ------ | ----------------------------------------------- | 18 | | searchValue | string | The current filled in value of the search input | 19 | 20 | ## Events 21 | 22 | | Name | Description | 23 | | ------ | ------------------------------------ | 24 | | search | Fires when the user wishes to search | 25 | 26 | ## Property Examples 27 | 28 | ### searchValue 29 | 30 | ** getting search value ** 31 | 32 | ```js 33 | // user writes "button" into the input field 34 | expect(el.searchValue).to.equal('button'); 35 | ``` 36 | 37 | ** search value in templates ** 38 | 39 | ```js 40 | 41 | // users sees "button" filled in the search input 42 | ``` 43 | 44 | ** setting search value ** 45 | 46 | ```js 47 | el.searchValue = 'button'; 48 | // users sees "button" filled in the search input 49 | ``` 50 | -------------------------------------------------------------------------------- /packages/intro/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | // import { forceReRender } from '@storybook/polymer'; 2 | import { html } from 'lit-html'; 3 | import mdx from './index.mdx'; 4 | 5 | import '../src/owc-cat-intro.js'; 6 | 7 | export default { 8 | title: 'Intro', 9 | parameters: { 10 | docs: { 11 | page: mdx, 12 | }, 13 | }, 14 | }; 15 | 16 | export const standard = () => 17 | html` 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /packages/item/src/github-markdown.css.js: -------------------------------------------------------------------------------- 1 | function css(strings, ...values) { 2 | return values.reduce((acc, v, idx) => acc + v + strings[idx + 1], strings[0]); 3 | } 4 | 5 | export default css` 6 | @font-face { 7 | font-family: octicons-link; 8 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) 9 | format('woff'); 10 | } 11 | 12 | .markdown-body .octicon { 13 | display: inline-block; 14 | fill: currentColor; 15 | vertical-align: text-bottom; 16 | } 17 | 18 | .markdown-body .anchor { 19 | float: left; 20 | line-height: 1; 21 | margin-left: -20px; 22 | padding-right: 4px; 23 | } 24 | 25 | .markdown-body .anchor:focus { 26 | outline: none; 27 | } 28 | 29 | .markdown-body h1 .octicon-link, 30 | .markdown-body h2 .octicon-link, 31 | .markdown-body h3 .octicon-link, 32 | .markdown-body h4 .octicon-link, 33 | .markdown-body h5 .octicon-link, 34 | .markdown-body h6 .octicon-link { 35 | color: #1b1f23; 36 | vertical-align: middle; 37 | visibility: hidden; 38 | } 39 | 40 | .markdown-body h1:hover .anchor, 41 | .markdown-body h2:hover .anchor, 42 | .markdown-body h3:hover .anchor, 43 | .markdown-body h4:hover .anchor, 44 | .markdown-body h5:hover .anchor, 45 | .markdown-body h6:hover .anchor { 46 | text-decoration: none; 47 | } 48 | 49 | .markdown-body h1:hover .anchor .octicon-link, 50 | .markdown-body h2:hover .anchor .octicon-link, 51 | .markdown-body h3:hover .anchor .octicon-link, 52 | .markdown-body h4:hover .anchor .octicon-link, 53 | .markdown-body h5:hover .anchor .octicon-link, 54 | .markdown-body h6:hover .anchor .octicon-link { 55 | visibility: visible; 56 | } 57 | 58 | .markdown-body { 59 | -ms-text-size-adjust: 100%; 60 | -webkit-text-size-adjust: 100%; 61 | color: #24292e; 62 | line-height: 1.5; 63 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, 64 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 65 | font-size: 16px; 66 | line-height: 1.5; 67 | word-wrap: break-word; 68 | } 69 | 70 | .markdown-body .pl-c { 71 | color: #6a737d; 72 | } 73 | 74 | .markdown-body .pl-c1, 75 | .markdown-body .pl-s .pl-v { 76 | color: #005cc5; 77 | } 78 | 79 | .markdown-body .pl-e, 80 | .markdown-body .pl-en { 81 | color: #6f42c1; 82 | } 83 | 84 | .markdown-body .pl-s .pl-s1, 85 | .markdown-body .pl-smi { 86 | color: #24292e; 87 | } 88 | 89 | .markdown-body .pl-ent { 90 | color: #22863a; 91 | } 92 | 93 | .markdown-body .pl-k { 94 | color: #d73a49; 95 | } 96 | 97 | .markdown-body .pl-pds, 98 | .markdown-body .pl-s, 99 | .markdown-body .pl-s .pl-pse .pl-s1, 100 | .markdown-body .pl-sr, 101 | .markdown-body .pl-sr .pl-cce, 102 | .markdown-body .pl-sr .pl-sra, 103 | .markdown-body .pl-sr .pl-sre { 104 | color: #032f62; 105 | } 106 | 107 | .markdown-body .pl-smw, 108 | .markdown-body .pl-v { 109 | color: #e36209; 110 | } 111 | 112 | .markdown-body .pl-bu { 113 | color: #b31d28; 114 | } 115 | 116 | .markdown-body .pl-ii { 117 | background-color: #b31d28; 118 | color: #fafbfc; 119 | } 120 | 121 | .markdown-body .pl-c2 { 122 | background-color: #d73a49; 123 | color: #fafbfc; 124 | } 125 | 126 | .markdown-body .pl-c2:before { 127 | content: '^M'; 128 | } 129 | 130 | .markdown-body .pl-sr .pl-cce { 131 | color: #22863a; 132 | font-weight: 700; 133 | } 134 | 135 | .markdown-body .pl-ml { 136 | color: #735c0f; 137 | } 138 | 139 | .markdown-body .pl-mh, 140 | .markdown-body .pl-mh .pl-en, 141 | .markdown-body .pl-ms { 142 | color: #005cc5; 143 | font-weight: 700; 144 | } 145 | 146 | .markdown-body .pl-mi { 147 | color: #24292e; 148 | font-style: italic; 149 | } 150 | 151 | .markdown-body .pl-mb { 152 | color: #24292e; 153 | font-weight: 700; 154 | } 155 | 156 | .markdown-body .pl-md { 157 | background-color: #ffeef0; 158 | color: #b31d28; 159 | } 160 | 161 | .markdown-body .pl-mi1 { 162 | background-color: #f0fff4; 163 | color: #22863a; 164 | } 165 | 166 | .markdown-body .pl-mc { 167 | background-color: #ffebda; 168 | color: #e36209; 169 | } 170 | 171 | .markdown-body .pl-mi2 { 172 | background-color: #005cc5; 173 | color: #f6f8fa; 174 | } 175 | 176 | .markdown-body .pl-mdr { 177 | color: #6f42c1; 178 | font-weight: 700; 179 | } 180 | 181 | .markdown-body .pl-ba { 182 | color: #586069; 183 | } 184 | 185 | .markdown-body .pl-sg { 186 | color: #959da5; 187 | } 188 | 189 | .markdown-body .pl-corl { 190 | color: #032f62; 191 | text-decoration: underline; 192 | } 193 | 194 | .markdown-body details { 195 | display: block; 196 | } 197 | 198 | .markdown-body summary { 199 | display: list-item; 200 | } 201 | 202 | .markdown-body a { 203 | background-color: transparent; 204 | } 205 | 206 | .markdown-body a:active, 207 | .markdown-body a:hover { 208 | outline-width: 0; 209 | } 210 | 211 | .markdown-body strong { 212 | font-weight: inherit; 213 | font-weight: bolder; 214 | } 215 | 216 | .markdown-body h1 { 217 | font-size: 2em; 218 | margin: 0.67em 0; 219 | } 220 | 221 | .markdown-body img { 222 | border-style: none; 223 | } 224 | 225 | .markdown-body code, 226 | .markdown-body kbd, 227 | .markdown-body pre { 228 | font-family: monospace, monospace; 229 | font-size: 1em; 230 | } 231 | 232 | .markdown-body hr { 233 | box-sizing: content-box; 234 | height: 0; 235 | overflow: visible; 236 | } 237 | 238 | .markdown-body input { 239 | font: inherit; 240 | margin: 0; 241 | } 242 | 243 | .markdown-body input { 244 | overflow: visible; 245 | } 246 | 247 | .markdown-body [type='checkbox'] { 248 | box-sizing: border-box; 249 | padding: 0; 250 | } 251 | 252 | .markdown-body * { 253 | box-sizing: border-box; 254 | } 255 | 256 | .markdown-body input { 257 | font-family: inherit; 258 | font-size: inherit; 259 | line-height: inherit; 260 | } 261 | 262 | .markdown-body a { 263 | color: #0366d6; 264 | text-decoration: none; 265 | } 266 | 267 | .markdown-body a:hover { 268 | text-decoration: underline; 269 | } 270 | 271 | .markdown-body strong { 272 | font-weight: 600; 273 | } 274 | 275 | .markdown-body hr { 276 | background: transparent; 277 | border: 0; 278 | border-bottom: 1px solid #dfe2e5; 279 | height: 0; 280 | margin: 15px 0; 281 | overflow: hidden; 282 | } 283 | 284 | .markdown-body hr:before { 285 | content: ''; 286 | display: table; 287 | } 288 | 289 | .markdown-body hr:after { 290 | clear: both; 291 | content: ''; 292 | display: table; 293 | } 294 | 295 | .markdown-body table { 296 | border-collapse: collapse; 297 | border-spacing: 0; 298 | } 299 | 300 | .markdown-body td, 301 | .markdown-body th { 302 | padding: 0; 303 | } 304 | 305 | .markdown-body details summary { 306 | cursor: pointer; 307 | } 308 | 309 | .markdown-body h1, 310 | .markdown-body h2, 311 | .markdown-body h3, 312 | .markdown-body h4, 313 | .markdown-body h5, 314 | .markdown-body h6 { 315 | margin-bottom: 0; 316 | margin-top: 0; 317 | } 318 | 319 | .markdown-body h1 { 320 | font-size: 32px; 321 | } 322 | 323 | .markdown-body h1, 324 | .markdown-body h2 { 325 | font-weight: 600; 326 | } 327 | 328 | .markdown-body h2 { 329 | font-size: 24px; 330 | } 331 | 332 | .markdown-body h3 { 333 | font-size: 20px; 334 | } 335 | 336 | .markdown-body h3, 337 | .markdown-body h4 { 338 | font-weight: 600; 339 | } 340 | 341 | .markdown-body h4 { 342 | font-size: 16px; 343 | } 344 | 345 | .markdown-body h5 { 346 | font-size: 14px; 347 | } 348 | 349 | .markdown-body h5, 350 | .markdown-body h6 { 351 | font-weight: 600; 352 | } 353 | 354 | .markdown-body h6 { 355 | font-size: 12px; 356 | } 357 | 358 | .markdown-body p { 359 | margin-bottom: 10px; 360 | margin-top: 0; 361 | } 362 | 363 | .markdown-body blockquote { 364 | margin: 0; 365 | } 366 | 367 | .markdown-body ol, 368 | .markdown-body ul { 369 | margin-bottom: 0; 370 | margin-top: 0; 371 | padding-left: 0; 372 | } 373 | 374 | .markdown-body ol ol, 375 | .markdown-body ul ol { 376 | list-style-type: lower-roman; 377 | } 378 | 379 | .markdown-body ol ol ol, 380 | .markdown-body ol ul ol, 381 | .markdown-body ul ol ol, 382 | .markdown-body ul ul ol { 383 | list-style-type: lower-alpha; 384 | } 385 | 386 | .markdown-body dd { 387 | margin-left: 0; 388 | } 389 | 390 | .markdown-body code, 391 | .markdown-body pre { 392 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 393 | font-size: 12px; 394 | } 395 | 396 | .markdown-body pre { 397 | margin-bottom: 0; 398 | margin-top: 0; 399 | } 400 | 401 | .markdown-body input::-webkit-inner-spin-button, 402 | .markdown-body input::-webkit-outer-spin-button { 403 | -webkit-appearance: none; 404 | appearance: none; 405 | margin: 0; 406 | } 407 | 408 | .markdown-body .border { 409 | border: 1px solid #e1e4e8 !important; 410 | } 411 | 412 | .markdown-body .border-0 { 413 | border: 0 !important; 414 | } 415 | 416 | .markdown-body .border-bottom { 417 | border-bottom: 1px solid #e1e4e8 !important; 418 | } 419 | 420 | .markdown-body .rounded-1 { 421 | border-radius: 3px !important; 422 | } 423 | 424 | .markdown-body .bg-white { 425 | background-color: #fff !important; 426 | } 427 | 428 | .markdown-body .bg-gray-light { 429 | background-color: #fafbfc !important; 430 | } 431 | 432 | .markdown-body .text-gray-light { 433 | color: #6a737d !important; 434 | } 435 | 436 | .markdown-body .mb-0 { 437 | margin-bottom: 0 !important; 438 | } 439 | 440 | .markdown-body .my-2 { 441 | margin-bottom: 8px !important; 442 | margin-top: 8px !important; 443 | } 444 | 445 | .markdown-body .pl-0 { 446 | padding-left: 0 !important; 447 | } 448 | 449 | .markdown-body .py-0 { 450 | padding-bottom: 0 !important; 451 | padding-top: 0 !important; 452 | } 453 | 454 | .markdown-body .pl-1 { 455 | padding-left: 4px !important; 456 | } 457 | 458 | .markdown-body .pl-2 { 459 | padding-left: 8px !important; 460 | } 461 | 462 | .markdown-body .py-2 { 463 | padding-bottom: 8px !important; 464 | padding-top: 8px !important; 465 | } 466 | 467 | .markdown-body .pl-3, 468 | .markdown-body .px-3 { 469 | padding-left: 16px !important; 470 | } 471 | 472 | .markdown-body .px-3 { 473 | padding-right: 16px !important; 474 | } 475 | 476 | .markdown-body .pl-4 { 477 | padding-left: 24px !important; 478 | } 479 | 480 | .markdown-body .pl-5 { 481 | padding-left: 32px !important; 482 | } 483 | 484 | .markdown-body .pl-6 { 485 | padding-left: 40px !important; 486 | } 487 | 488 | .markdown-body .f6 { 489 | font-size: 12px !important; 490 | } 491 | 492 | .markdown-body .lh-condensed { 493 | line-height: 1.25 !important; 494 | } 495 | 496 | .markdown-body .text-bold { 497 | font-weight: 600 !important; 498 | } 499 | 500 | .markdown-body:before { 501 | content: ''; 502 | display: table; 503 | } 504 | 505 | .markdown-body:after { 506 | clear: both; 507 | content: ''; 508 | display: table; 509 | } 510 | 511 | .markdown-body > :first-child { 512 | margin-top: 0 !important; 513 | } 514 | 515 | .markdown-body > :last-child { 516 | margin-bottom: 0 !important; 517 | } 518 | 519 | .markdown-body a:not([href]) { 520 | color: inherit; 521 | text-decoration: none; 522 | } 523 | 524 | .markdown-body blockquote, 525 | .markdown-body dl, 526 | .markdown-body ol, 527 | .markdown-body p, 528 | .markdown-body pre, 529 | .markdown-body table, 530 | .markdown-body ul { 531 | margin-bottom: 16px; 532 | margin-top: 0; 533 | } 534 | 535 | .markdown-body hr { 536 | background-color: #e1e4e8; 537 | border: 0; 538 | height: 0.25em; 539 | margin: 24px 0; 540 | padding: 0; 541 | } 542 | 543 | .markdown-body blockquote { 544 | border-left: 0.25em solid #dfe2e5; 545 | color: #6a737d; 546 | padding: 0 1em; 547 | } 548 | 549 | .markdown-body blockquote > :first-child { 550 | margin-top: 0; 551 | } 552 | 553 | .markdown-body blockquote > :last-child { 554 | margin-bottom: 0; 555 | } 556 | 557 | .markdown-body kbd { 558 | background-color: #fafbfc; 559 | border: 1px solid #c6cbd1; 560 | border-bottom-color: #959da5; 561 | border-radius: 3px; 562 | box-shadow: inset 0 -1px 0 #959da5; 563 | color: #444d56; 564 | display: inline-block; 565 | font-size: 11px; 566 | line-height: 10px; 567 | padding: 3px 5px; 568 | vertical-align: middle; 569 | } 570 | 571 | .markdown-body h1, 572 | .markdown-body h2, 573 | .markdown-body h3, 574 | .markdown-body h4, 575 | .markdown-body h5, 576 | .markdown-body h6 { 577 | font-weight: 600; 578 | line-height: 1.25; 579 | margin-bottom: 16px; 580 | margin-top: 24px; 581 | } 582 | 583 | .markdown-body h1 { 584 | font-size: 2em; 585 | } 586 | 587 | .markdown-body h1, 588 | .markdown-body h2 { 589 | border-bottom: 1px solid #eaecef; 590 | padding-bottom: 0.3em; 591 | } 592 | 593 | .markdown-body h2 { 594 | font-size: 1.5em; 595 | } 596 | 597 | .markdown-body h3 { 598 | font-size: 1.25em; 599 | } 600 | 601 | .markdown-body h4 { 602 | font-size: 1em; 603 | } 604 | 605 | .markdown-body h5 { 606 | font-size: 0.875em; 607 | } 608 | 609 | .markdown-body h6 { 610 | color: #6a737d; 611 | font-size: 0.85em; 612 | } 613 | 614 | .markdown-body ol, 615 | .markdown-body ul { 616 | padding-left: 2em; 617 | } 618 | 619 | .markdown-body ol ol, 620 | .markdown-body ol ul, 621 | .markdown-body ul ol, 622 | .markdown-body ul ul { 623 | margin-bottom: 0; 624 | margin-top: 0; 625 | } 626 | 627 | .markdown-body li { 628 | word-wrap: break-all; 629 | } 630 | 631 | .markdown-body li > p { 632 | margin-top: 16px; 633 | } 634 | 635 | .markdown-body li + li { 636 | margin-top: 0.25em; 637 | } 638 | 639 | .markdown-body dl { 640 | padding: 0; 641 | } 642 | 643 | .markdown-body dl dt { 644 | font-size: 1em; 645 | font-style: italic; 646 | font-weight: 600; 647 | margin-top: 16px; 648 | padding: 0; 649 | } 650 | 651 | .markdown-body dl dd { 652 | margin-bottom: 16px; 653 | padding: 0 16px; 654 | } 655 | 656 | .markdown-body table { 657 | display: block; 658 | overflow: auto; 659 | width: 100%; 660 | } 661 | 662 | .markdown-body table th { 663 | font-weight: 600; 664 | } 665 | 666 | .markdown-body table td, 667 | .markdown-body table th { 668 | border: 1px solid #dfe2e5; 669 | padding: 6px 13px; 670 | } 671 | 672 | .markdown-body table tr { 673 | background-color: #fff; 674 | border-top: 1px solid #c6cbd1; 675 | } 676 | 677 | .markdown-body table tr:nth-child(2n) { 678 | background-color: #f6f8fa; 679 | } 680 | 681 | .markdown-body img { 682 | background-color: #fff; 683 | box-sizing: content-box; 684 | max-width: 100%; 685 | } 686 | 687 | .markdown-body img[align='right'] { 688 | padding-left: 20px; 689 | } 690 | 691 | .markdown-body img[align='left'] { 692 | padding-right: 20px; 693 | } 694 | 695 | .markdown-body code { 696 | background-color: rgba(27, 31, 35, 0.05); 697 | border-radius: 3px; 698 | font-size: 85%; 699 | margin: 0; 700 | padding: 0.2em 0.4em; 701 | } 702 | 703 | .markdown-body pre { 704 | word-wrap: normal; 705 | } 706 | 707 | .markdown-body pre > code { 708 | background: transparent; 709 | border: 0; 710 | font-size: 100%; 711 | margin: 0; 712 | padding: 0; 713 | white-space: pre; 714 | word-break: normal; 715 | } 716 | 717 | .markdown-body .highlight { 718 | margin-bottom: 16px; 719 | } 720 | 721 | .markdown-body .highlight pre { 722 | margin-bottom: 0; 723 | word-break: normal; 724 | } 725 | 726 | .markdown-body .highlight pre, 727 | .markdown-body pre { 728 | background-color: #f6f8fa; 729 | border-radius: 3px; 730 | font-size: 85%; 731 | line-height: 1.45; 732 | overflow: auto; 733 | padding: 16px; 734 | } 735 | 736 | .markdown-body pre code { 737 | background-color: transparent; 738 | border: 0; 739 | display: inline; 740 | line-height: inherit; 741 | margin: 0; 742 | max-width: auto; 743 | overflow: visible; 744 | padding: 0; 745 | word-wrap: normal; 746 | } 747 | 748 | .markdown-body .commit-tease-sha { 749 | color: #444d56; 750 | display: inline-block; 751 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 752 | font-size: 90%; 753 | } 754 | 755 | .markdown-body .blob-wrapper { 756 | border-bottom-left-radius: 3px; 757 | border-bottom-right-radius: 3px; 758 | overflow-x: auto; 759 | overflow-y: hidden; 760 | } 761 | 762 | .markdown-body .blob-wrapper-embedded { 763 | max-height: 240px; 764 | overflow-y: auto; 765 | } 766 | 767 | .markdown-body .blob-num { 768 | -moz-user-select: none; 769 | -ms-user-select: none; 770 | -webkit-user-select: none; 771 | color: rgba(27, 31, 35, 0.3); 772 | cursor: pointer; 773 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 774 | font-size: 12px; 775 | line-height: 20px; 776 | min-width: 50px; 777 | padding-left: 10px; 778 | padding-right: 10px; 779 | text-align: right; 780 | user-select: none; 781 | vertical-align: top; 782 | white-space: nowrap; 783 | width: 1%; 784 | } 785 | 786 | .markdown-body .blob-num:hover { 787 | color: rgba(27, 31, 35, 0.6); 788 | } 789 | 790 | .markdown-body .blob-num:before { 791 | content: attr(data-line-number); 792 | } 793 | 794 | .markdown-body .blob-code { 795 | line-height: 20px; 796 | padding-left: 10px; 797 | padding-right: 10px; 798 | position: relative; 799 | vertical-align: top; 800 | } 801 | 802 | .markdown-body .blob-code-inner { 803 | color: #24292e; 804 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 805 | font-size: 12px; 806 | overflow: visible; 807 | white-space: pre; 808 | word-wrap: normal; 809 | } 810 | 811 | .markdown-body .pl-token.active, 812 | .markdown-body .pl-token:hover { 813 | background: #ffea7f; 814 | cursor: pointer; 815 | } 816 | 817 | .markdown-body kbd { 818 | background-color: #fafbfc; 819 | border: 1px solid #d1d5da; 820 | border-bottom-color: #c6cbd1; 821 | border-radius: 3px; 822 | box-shadow: inset 0 -1px 0 #c6cbd1; 823 | color: #444d56; 824 | display: inline-block; 825 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 826 | line-height: 10px; 827 | padding: 3px 5px; 828 | vertical-align: middle; 829 | } 830 | 831 | .markdown-body :checked + .radio-label { 832 | border-color: #0366d6; 833 | position: relative; 834 | z-index: 1; 835 | } 836 | 837 | .markdown-body .tab-size[data-tab-size='1'] { 838 | -moz-tab-size: 1; 839 | tab-size: 1; 840 | } 841 | 842 | .markdown-body .tab-size[data-tab-size='2'] { 843 | -moz-tab-size: 2; 844 | tab-size: 2; 845 | } 846 | 847 | .markdown-body .tab-size[data-tab-size='3'] { 848 | -moz-tab-size: 3; 849 | tab-size: 3; 850 | } 851 | 852 | .markdown-body .tab-size[data-tab-size='4'] { 853 | -moz-tab-size: 4; 854 | tab-size: 4; 855 | } 856 | 857 | .markdown-body .tab-size[data-tab-size='5'] { 858 | -moz-tab-size: 5; 859 | tab-size: 5; 860 | } 861 | 862 | .markdown-body .tab-size[data-tab-size='6'] { 863 | -moz-tab-size: 6; 864 | tab-size: 6; 865 | } 866 | 867 | .markdown-body .tab-size[data-tab-size='7'] { 868 | -moz-tab-size: 7; 869 | tab-size: 7; 870 | } 871 | 872 | .markdown-body .tab-size[data-tab-size='8'] { 873 | -moz-tab-size: 8; 874 | tab-size: 8; 875 | } 876 | 877 | .markdown-body .tab-size[data-tab-size='9'] { 878 | -moz-tab-size: 9; 879 | tab-size: 9; 880 | } 881 | 882 | .markdown-body .tab-size[data-tab-size='10'] { 883 | -moz-tab-size: 10; 884 | tab-size: 10; 885 | } 886 | 887 | .markdown-body .tab-size[data-tab-size='11'] { 888 | -moz-tab-size: 11; 889 | tab-size: 11; 890 | } 891 | 892 | .markdown-body .tab-size[data-tab-size='12'] { 893 | -moz-tab-size: 12; 894 | tab-size: 12; 895 | } 896 | 897 | .markdown-body .task-list-item { 898 | list-style-type: none; 899 | } 900 | 901 | .markdown-body .task-list-item + .task-list-item { 902 | margin-top: 3px; 903 | } 904 | 905 | .markdown-body .task-list-item input { 906 | margin: 0 0.2em 0.25em -1.6em; 907 | vertical-align: middle; 908 | } 909 | 910 | .markdown-body hr { 911 | border-bottom-color: #eee; 912 | } 913 | 914 | .markdown-body .pl-0 { 915 | padding-left: 0 !important; 916 | } 917 | 918 | .markdown-body .pl-1 { 919 | padding-left: 4px !important; 920 | } 921 | 922 | .markdown-body .pl-2 { 923 | padding-left: 8px !important; 924 | } 925 | 926 | .markdown-body .pl-3 { 927 | padding-left: 16px !important; 928 | } 929 | 930 | .markdown-body .pl-4 { 931 | padding-left: 24px !important; 932 | } 933 | 934 | .markdown-body .pl-5 { 935 | padding-left: 32px !important; 936 | } 937 | 938 | .markdown-body .pl-6 { 939 | padding-left: 40px !important; 940 | } 941 | 942 | .markdown-body .pl-7 { 943 | padding-left: 48px !important; 944 | } 945 | 946 | .markdown-body .pl-8 { 947 | padding-left: 64px !important; 948 | } 949 | 950 | .markdown-body .pl-9 { 951 | padding-left: 80px !important; 952 | } 953 | 954 | .markdown-body .pl-10 { 955 | padding-left: 96px !important; 956 | } 957 | 958 | .markdown-body .pl-11 { 959 | padding-left: 112px !important; 960 | } 961 | 962 | .markdown-body .pl-12 { 963 | padding-left: 128px !important; 964 | } 965 | 966 | /** added hacks */ 967 | .markdown-body pre { 968 | max-width: calc(100vw - 10px); 969 | } 970 | `; 971 | -------------------------------------------------------------------------------- /packages/item/src/logos/logo-github.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | class LogoGithub extends LitElement { 4 | static get styles() { 5 | return css` 6 | :host { 7 | display: block; 8 | } 9 | `; 10 | } 11 | 12 | // eslint-disable-next-line 13 | render() { 14 | return html`github`; 15 | } 16 | } 17 | customElements.define('logo-github', LogoGithub); 18 | -------------------------------------------------------------------------------- /packages/item/src/logos/logo-npm.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | class LogoNpm extends LitElement { 4 | static get styles() { 5 | return css` 6 | :host { 7 | display: block; 8 | width: 100%; 9 | } 10 | 11 | svg { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | `; 16 | } 17 | 18 | // eslint-disable-next-line 19 | render() { 20 | return html`npm`; 21 | } 22 | } 23 | customElements.define('logo-npm', LogoNpm); 24 | -------------------------------------------------------------------------------- /packages/item/src/logos/logo-unpkg.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | class LogoUnpkg extends LitElement { 4 | static get styles() { 5 | return css` 6 | :host { 7 | display: block; 8 | } 9 | `; 10 | } 11 | 12 | // eslint-disable-next-line 13 | render() { 14 | return html`unpkg`; 15 | } 16 | } 17 | customElements.define('logo-unpkg', LogoUnpkg); 18 | -------------------------------------------------------------------------------- /packages/item/src/owc-cat-item.css.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | :host { 5 | --owc-blue: #217ff9; 6 | --owc-purple: #aa00ff; 7 | text-align: left; 8 | font-size: 15px; 9 | padding: 15px; 10 | background-color: white; 11 | min-height: 85px; 12 | box-sizing: border-box; 13 | margin: 15px 10px; 14 | border: 1px solid #ccc; 15 | display: block; 16 | } 17 | 18 | h1 { 19 | color: var(--owc-blue); 20 | margin-top: 0; 21 | margin-bottom: 10px; 22 | margin-right: 30px; 23 | font-family: 'Roboto'; 24 | font-size: 22px; 25 | } 26 | 27 | h1 a { 28 | color: var(--owc-blue); 29 | text-decoration: none; 30 | } 31 | 32 | h1 a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | .big { 37 | font-size: 22px; 38 | font-weight: bold; 39 | margin-bottom: 4px; 40 | } 41 | 42 | .big--not-so-much { 43 | font-size: 20px; 44 | } 45 | 46 | .small { 47 | font-size: 13px; 48 | } 49 | 50 | .unit { 51 | color: #777; 52 | fill: #777; 53 | font-size: 18px; 54 | } 55 | 56 | a { 57 | text-decoration: none; 58 | color: inherit; 59 | } 60 | 61 | p { 62 | margin: 0; 63 | text-align: center; 64 | } 65 | 66 | iframe { 67 | width: 100%; 68 | border: none; 69 | height: calc(100vh - 132px); 70 | } 71 | 72 | owc-tabs { 73 | width: calc(100vw - 20px); 74 | } 75 | 76 | #badges { 77 | margin-top: 10px; 78 | } 79 | 80 | .desktop { 81 | display: none; 82 | } 83 | 84 | #details { 85 | display: none; 86 | } 87 | 88 | :host([show-details]) #details { 89 | display: block; 90 | } 91 | 92 | :host([show-details]) { 93 | margin: 0; 94 | padding: 10px; 95 | min-height: 100vh; 96 | width: 100vw; 97 | border-width: 0 1px; 98 | } 99 | 100 | :host([show-details]) h1 { 101 | margin-bottom: 10px; 102 | } 103 | 104 | :host([show-details]) #overview { 105 | display: none; 106 | } 107 | 108 | /* Grid */ 109 | 110 | #overview, 111 | .info-grid { 112 | display: grid; 113 | grid-gap: 3px 10px; 114 | grid-template-columns: 1fr 1fr 1fr; 115 | grid-template-areas: 116 | 'info info info' 117 | 'dependencies githubStars lastRelease' 118 | 'downloadsNpm sizeGzip lastRelease'; 119 | align-items: center; 120 | } 121 | 122 | #info { 123 | grid-area: info; 124 | } 125 | 126 | #lastRelease { 127 | grid-area: lastRelease; 128 | } 129 | 130 | #downloadsNpm { 131 | grid-area: downloadsNpm; 132 | } 133 | 134 | #dependencies { 135 | grid-area: dependencies; 136 | } 137 | 138 | #sizeGzip { 139 | grid-area: sizeGzip; 140 | } 141 | 142 | #githubStars { 143 | grid-area: githubStars; 144 | } 145 | 146 | #details { 147 | grid-area: details; 148 | } 149 | 150 | #links { 151 | display: grid; 152 | grid-template-columns: 130px 130px; 153 | grid-gap: 28px; 154 | } 155 | 156 | .link { 157 | border: 1px solid #ccc; 158 | width: 130px; 159 | height: 130px; 160 | } 161 | 162 | .link a { 163 | display: flex; 164 | flex-flow: column; 165 | height: 100%; 166 | align-items: center; 167 | justify-content: center; 168 | } 169 | 170 | .link__logo { 171 | width: 70px; 172 | height: 70px; 173 | margin-bottom: 10px; 174 | } 175 | 176 | .fake-url-bar { 177 | border: 7px solid #929292; 178 | display: block; 179 | padding: 5px; 180 | } 181 | 182 | @media only screen and (min-width: 768px) { 183 | h1 { 184 | font-size: 26px; 185 | } 186 | 187 | owc-tabs { 188 | width: auto; 189 | } 190 | 191 | #links { 192 | grid-template-columns: 130px 130px 130px 130px; 193 | } 194 | 195 | :host { 196 | border-radius: 10px; 197 | border: none; 198 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); 199 | } 200 | 201 | :host #overview { 202 | grid-gap: 20px; 203 | grid-template-columns: 4fr 1fr 1fr 1fr 1fr 1fr; 204 | grid-template-areas: 'info sizeGzip githubStars dependencies lastRelease downloadsNpm'; 205 | } 206 | 207 | #lastRelease { 208 | min-width: 100px; 209 | } 210 | 211 | :host([show-details]) #overview { 212 | display: grid; 213 | } 214 | 215 | #details { 216 | margin-top: 15px; 217 | } 218 | 219 | :host([show-details]) { 220 | position: static; 221 | min-height: auto; 222 | min-width: auto; 223 | padding: 15px; 224 | margin: 15px 10px; 225 | width: auto; 226 | } 227 | 228 | :host([show-details]) h1 { 229 | margin-bottom: 10px; 230 | } 231 | 232 | .mobile { 233 | display: none; 234 | } 235 | 236 | .desktop { 237 | display: block; 238 | } 239 | 240 | .desktop--inline { 241 | display: inline; 242 | } 243 | } 244 | `; 245 | -------------------------------------------------------------------------------- /packages/item/src/owc-cat-item.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit-element'; 2 | 3 | import '@github/time-elements'; 4 | 5 | import './remarkable-markdown.js'; 6 | import './owc-tabs.js'; 7 | 8 | import './logos/logo-github.js'; 9 | import './logos/logo-npm.js'; 10 | import './logos/logo-unpkg.js'; 11 | 12 | import owcCatItemStyle from './owc-cat-item.css.js'; 13 | 14 | const githubStar = html` 15 | 21 | `; 22 | 23 | export class OwcCatItem extends LitElement { 24 | static get properties() { 25 | return { 26 | name: { type: String }, 27 | description: { type: String }, 28 | size: { type: String }, 29 | sizeGzip: { type: String }, 30 | version: { type: String }, 31 | versionTime: { type: String }, 32 | flattenedDependencies: { type: Array }, 33 | npmUrl: { type: String }, 34 | unpkgUrl: { type: String }, 35 | githubStars: { type: Number }, 36 | githubUrl: { type: String }, 37 | bundlephobiaUrl: { type: String }, 38 | demoUrl: { type: String }, 39 | showDetails: { type: Boolean, attribute: 'show-details', reflect: true }, 40 | detailsTabIndex: { type: Number }, 41 | }; 42 | } 43 | 44 | constructor() { 45 | super(); 46 | /** 47 | * A flag which decided if to show the overview or details style 48 | */ 49 | this.showDetails = false; 50 | /** 51 | * Index of the open details tab 52 | * 0. Info 53 | * 1. Readme 54 | * 2. Demo 55 | * 3. Links 56 | * 4. Source 57 | */ 58 | this.detailsTabIndex = 1; 59 | /** 60 | * Url that get shown in the demo tab 61 | */ 62 | this.demoUrl = ''; 63 | /** 64 | * A list of all flattened dependencies with versions. 65 | * 66 | * e.g. ['lit-element@2.x.x', 'lit-element@2.1.x', ...] 67 | * 68 | * @type {string[]} 69 | */ 70 | this.flattenedDependencies = []; 71 | /** 72 | * The name of the web component. Needs to have a "-" in it. 73 | */ 74 | this.name = ''; 75 | /** 76 | * A Semver Version like "1.0.1" or "2.11.40" 77 | */ 78 | this.version = ''; 79 | this.description = ''; 80 | this.size = 0; 81 | this.sizeGzip = 0; 82 | this.unpkgUrl = ''; 83 | this.npmUrl = ''; 84 | this.githubStars = 0; 85 | this.githubUrl = ''; 86 | this.bundlephobiaUrl = ''; 87 | 88 | this.addEventListener('click', this._handleClick.bind(this)); 89 | } 90 | 91 | get dependenciesCount() { 92 | return this.flattenedDependencies.length / 4; 93 | } 94 | 95 | _handleClick(ev) { 96 | const path = ev.composedPath(); 97 | if (!path.includes(this.shadowRoot.querySelector('#details'))) { 98 | this.toggle(ev); 99 | } 100 | if (path.includes(this.shadowRoot.querySelector('#details h1'))) { 101 | this.toggle(ev); 102 | } 103 | } 104 | 105 | toggle(ev) { 106 | if (ev) { 107 | ev.preventDefault(); 108 | } 109 | this.showDetails = !this.showDetails; 110 | this.dispatchEvent(new Event('showDetailsChanged')); 111 | } 112 | 113 | renderRegisteredTypes() { 114 | return html` 115 | ${this.flattenedDependencies.map(type => { 116 | switch (type) { 117 | case 'lit-element@2.x.x': 118 | return html` 119 | 122 | `; 123 | case 'haunted@4.x.x': 124 | return html` 125 | 128 | `; 129 | default: 130 | return html``; 131 | } 132 | })} 133 | `; 134 | } 135 | 136 | __syncDetailsTabIndex() { 137 | this.detailsTabIndex = this.shadowRoot.querySelector('owc-tabs').activeIndex; 138 | this.dispatchEvent(new Event('detailsTabIndexChanged')); 139 | } 140 | 141 | render() { 142 | return html` 143 |
144 |
145 |

146 | ${this.name}@${this.version} 147 |

148 | 149 |
150 | ${this.description} 151 |
152 | 153 |
154 | ${this.renderRegisteredTypes()} 155 |
156 |
157 | 158 |
159 |

${this.dependenciesCount}dep

160 |

161 | ${this.dependenciesCount === 1 ? 'dependency' : 'dependencies'} 162 |

163 |
164 | 165 |
166 |

167 | 168 | 169 | ago 170 |

171 |

released on npm

172 |
173 | 174 |
175 |

${'?'}dl

176 |

downloads on npm

177 |
178 | 179 |
180 |

${(this.sizeGzip / 1024).toFixed(2)}kB

181 |

size gzipped

182 |
183 | 184 |
185 |

${this.githubStars}${githubStar}

186 |

on Github

187 |
188 |
189 | 190 |
191 |

192 | ${this.name}@${this.version} 193 |

194 | 195 | 200 |
Info
201 |
202 |
203 |
Description
204 |
${this.description}
205 |
Badges
206 |
${this.renderRegisteredTypes()}
207 |
Size gzipped
208 |
${(this.sizeGzip / 1024).toFixed(2)}
209 |
Stars on Github
210 |
${this.githubStars}
211 |
Dependencies
212 |
${this.dependenciesCount}
213 |
Last release on npm
214 |
215 |
Downloads on npm
216 |
?
217 |
218 |
219 | 220 |
Readme
221 |
222 | 223 |
224 | 227 |
228 |
229 | 230 |
Demo
231 |
232 | ${this.demoUrl 233 | ? html` 234 | ${this.demoUrl} 235 | 236 | ` 237 | : `No demo could be found at demo/index.html. Neither a demoUrl in package.json was found. You can do it like that ...`} 238 |
239 | 240 |
Links
241 |
242 | 268 |
269 | 270 |
Source
271 |
272 | ${this.unpkgUrl} 273 | 274 |
275 |
276 |
277 | `; 278 | } 279 | 280 | static get styles() { 281 | return [owcCatItemStyle]; 282 | } 283 | } 284 | 285 | customElements.define('owc-cat-item', OwcCatItem); 286 | -------------------------------------------------------------------------------- /packages/item/src/owc-tabs.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | const debounce = (func, delay) => { 4 | let inDebounce; 5 | return function(...args) { 6 | const context = this; 7 | clearTimeout(inDebounce); 8 | inDebounce = setTimeout(() => func.apply(context, args), delay); 9 | }; 10 | }; 11 | 12 | export class OwcTabs extends LitElement { 13 | static get properties() { 14 | return { 15 | activeIndex: { type: Number }, 16 | mode: { type: String, reflect: true }, 17 | }; 18 | } 19 | 20 | get tabs() { 21 | return [...this.querySelectorAll('[slot=tab]')]; 22 | } 23 | 24 | get contents() { 25 | return [...this.querySelectorAll('[slot=tab-content]')]; 26 | } 27 | 28 | constructor() { 29 | super(); 30 | this.activeIndex = 0; 31 | this.mode = 'scroll'; // [scroll, display] 32 | 33 | this.addEventListener('click', this._onClick.bind(this)); 34 | } 35 | 36 | _onClick(ev) { 37 | const path = ev.composedPath(); 38 | const foundIndex = this.tabs.indexOf(path[0]); 39 | if (foundIndex !== -1) { 40 | this.activeIndex = foundIndex; 41 | this.dispatchEvent(new Event('activeIndexChanged')); 42 | } 43 | } 44 | 45 | __setAttributesForEls(activeIndex) { 46 | this.tabs.forEach((el, i) => { 47 | if (i === activeIndex) { 48 | el.setAttribute('owc-tabs-active', ''); 49 | } else { 50 | el.removeAttribute('owc-tabs-active'); 51 | } 52 | }); 53 | this.contents.forEach((el, i) => { 54 | if (i === activeIndex) { 55 | el.setAttribute('owc-tabs-active', ''); 56 | } else { 57 | el.removeAttribute('owc-tabs-active'); 58 | } 59 | }); 60 | } 61 | 62 | updated(changedProps) { 63 | super.updated(changedProps); 64 | if (changedProps.has('activeIndex')) { 65 | this.__setAttributesForEls(this.activeIndex); 66 | if (this.mode === 'scroll') { 67 | // this.contents[this.activeIndex].scrollIntoView({ 68 | // behavior: 'smooth', 69 | // }); 70 | 71 | this.wrapper.scrollLeft = this.contents[this.activeIndex].offsetLeft; 72 | this.tabsWrapper.scrollLeft = this.tabs[this.activeIndex].offsetLeft; 73 | } 74 | } 75 | } 76 | 77 | get tabsWrapper() { 78 | return this.shadowRoot.getElementById('tabs-wrapper'); 79 | } 80 | 81 | get wrapper() { 82 | return this.shadowRoot.getElementById('wrapper'); 83 | } 84 | 85 | connectedCallback() { 86 | super.connectedCallback(); 87 | } 88 | 89 | firstUpdated() { 90 | super.firstUpdated(); 91 | this.wrapper.addEventListener( 92 | 'scroll', 93 | debounce(() => { 94 | this.contents.forEach((el, i) => { 95 | if (this.wrapper.scrollLeft === el.offsetLeft) { 96 | this.activeIndex = i; 97 | } 98 | }); 99 | }, 100), 100 | ); 101 | } 102 | 103 | _updateSlotCount() { 104 | const visibleTabs = this.tabs.filter(tab => getComputedStyle(tab).display !== 'none'); 105 | this.style.setProperty('--owc-tabs-count', visibleTabs.length); 106 | } 107 | 108 | render() { 109 | return html` 110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 | `; 121 | } 122 | 123 | static get styles() { 124 | return css` 125 | :host { 126 | display: block; 127 | box-sizing: border-box; 128 | 129 | --owc-tabs-count: 2; 130 | } 131 | 132 | ::-webkit-scrollbar { 133 | height: 0; 134 | width: 0; 135 | background: transparent; 136 | } 137 | 138 | #tabs { 139 | display: flex; 140 | border-bottom: 2px solid #ccc; 141 | } 142 | 143 | #tabs ::slotted(*) { 144 | cursor: pointer; 145 | padding-bottom: 7px; 146 | margin-bottom: -2px; 147 | margin-right: 13px; 148 | text-transform: uppercase; 149 | } 150 | 151 | #tabs ::slotted([owc-tabs-active]) { 152 | border-bottom: 4px solid #217ff9; 153 | } 154 | 155 | #contents ::slotted(*) { 156 | display: none; 157 | } 158 | 159 | #contents ::slotted([owc-tabs-active]) { 160 | display: block; 161 | /* margin-top: 10px; */ 162 | } 163 | 164 | /* mode = scroll */ 165 | :host([mode='scroll']) #tabs-wrapper { 166 | overflow-x: scroll; 167 | scroll-behavior: smooth; 168 | } 169 | 170 | :host([mode='scroll']) #tabs { 171 | border: none; 172 | } 173 | 174 | /* :host([mode='scroll']) #tabs::before, 175 | :host([mode='scroll']) #tabs::after { 176 | content: ''; 177 | display: block; 178 | padding: 5px; 179 | } */ 180 | 181 | :host([mode='scroll']) #wrapper { 182 | scroll-snap-type: x mandatory; 183 | overflow-x: scroll; 184 | scroll-behavior: smooth; 185 | } 186 | 187 | :host([mode='scroll']) #contents { 188 | display: flex; 189 | /* flex-flow: column; */ 190 | width: calc(100% * var(--owc-tabs-count)); 191 | } 192 | 193 | :host([mode='scroll']) #contents ::slotted(*) { 194 | scroll-snap-align: start; 195 | display: block; 196 | /* width: 100%; */ 197 | } 198 | `; 199 | } 200 | } 201 | 202 | customElements.define('owc-tabs', OwcTabs); 203 | -------------------------------------------------------------------------------- /packages/item/src/remarkable-markdown.js: -------------------------------------------------------------------------------- 1 | import { Remarkable } from 'remarkable'; 2 | 3 | import githubMarkdownCss from './github-markdown.css.js'; 4 | 5 | /** 6 | * @param {string} text 7 | * @return {string} 8 | */ 9 | function unindent(_text) { 10 | const text = _text.trim(); 11 | if (!text) { 12 | return text; 13 | } 14 | const lines = text.replace(/\t/g, ' ').split('\n'); 15 | const indent = lines.reduce((prev, line) => { 16 | if (/^\s*$/.test(line)) { 17 | return prev; // Completely ignore blank lines. 18 | } 19 | 20 | const lineIndent = line.match(/^(\s*)/)[0].length; 21 | if (prev === null) { 22 | return lineIndent; 23 | } 24 | return lineIndent < prev ? lineIndent : prev; 25 | }, null); 26 | 27 | return lines.map(l => l.substr(indent)).join('\n'); 28 | } 29 | 30 | export class RemarkableMarkdown extends HTMLElement { 31 | constructor() { 32 | super(); 33 | this.attachShadow({ mode: 'open' }); 34 | } 35 | 36 | connectedCallback() { 37 | this.__md = new Remarkable(); 38 | 39 | this._srcEl = this.querySelector('[type="text/markdown"]'); 40 | if (!this._srcEl) { 41 | throw new Error('You need to provide'); 42 | } 43 | 44 | if (this._srcEl.text) { 45 | const src = unindent(this._srcEl.text); 46 | this.markdown = this.__md.render(src); 47 | } 48 | this.shadowRoot.innerHTML = ` 49 |
50 | ${this.markdown} 51 |
52 | 55 | `; 56 | } 57 | } 58 | 59 | customElements.define('remarkable-markdown', RemarkableMarkdown); 60 | -------------------------------------------------------------------------------- /packages/item/stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Preview, DocsStory } from '@storybook/addon-docs/blocks'; 2 | 3 | # Item 4 | 5 | This shows all the info users are actually after. 6 | 7 | ## List View 8 | 9 | In the list view only the most important information is shown 10 | 11 | - Name 12 | - Version 13 | - Description 14 | - Highlight known libraries (like lit-element, haunted, stencil, ...) 15 | - Download Time (how much time it will take to download it on 3G) 16 | - Gzipped Size in kB 17 | - Github stars 18 | 19 | With this, we hope to give you all the information you need to decide which web component 20 | look like a good fit for your use case. For each of those, you will hopefully explore the details. 21 | 22 | #### Single item 23 | 24 | 25 | 26 | 27 | 28 | #### Multiple items 29 | 30 | In most cases you will see multiple items after each other. 31 | 32 | 33 | 34 | 35 | 36 | ## Detail View 37 | 38 | Here we are adding more information. 39 | 40 | #### Readme 41 | 42 | This renders the README.md in the root of the package. 43 | 44 | 45 | 46 | 47 | 48 | #### Demo 49 | 50 | The demo is a very important part of every web component. 51 | By default, if we find a `demo/index.html` file we will show it via unpkg.com with the module flag. 52 | We do NOT do any magic on it. e.g. you will need to make sure it works directly via unpkg. 53 | This usually needs to use imports directly from unpkg. 54 | 55 | So if you wanna show demos with using properties you can do it like so. 56 | 57 | ```html 58 |
59 | 60 | 72 | ``` 73 | 74 | In case you wanna share the same demo with multiple packages you can set a `demoUrl` in your `package.json`. 75 | This demo url will take precedence over the default url. 76 | 77 | ```js 78 | // package.json 79 | { 80 | "demoUrl": "http://this-is-my-demo-2.0.1.domain.com" 81 | } 82 | ``` 83 | 84 | Note that this means that you will need to use versioning yourself if you wish to have different demos for different versions. 85 | 86 | If your demo becomes big you can also publish it as a separate npm package and link it with a demoUrl. 87 | For example you want to published `@lib/my-button@1.2.2` then you could publish `@lib-demos/my-button@1.2.2`. 88 | So setting the `demoUrl` on `@lib/my-button` to `https://unpkg.com/@lib-demos/my-button@1.2.2/index.html?module` will give you versioned 89 | demos without adding weight to your source package. 90 | 91 | Be sure to NOT have a `custom-element.json` in `@lib-demos` as you don't want to have this package indexed in the catalog. 92 | 93 | Ok finally let's look at an actual demo 94 | 95 | 96 | 97 | 98 | 99 | #### Links 100 | 101 | Here we link to some of the resource you might be interested in 102 | 103 | - Github 104 | - Npm 105 | - Bundlephobia 106 | - Unpkg 107 | 108 | 109 | 110 | 111 | 112 | #### Source 113 | 114 | Directly look at the source files at unpkg.com 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /packages/item/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | // import { forceReRender } from '@storybook/polymer'; 2 | import { html } from 'lit-html'; 3 | import mdx from './index.mdx'; 4 | 5 | import '../src/owc-cat-item.js'; 6 | 7 | export default { 8 | title: 'Item', 9 | parameters: { 10 | docs: { 11 | page: mdx, 12 | }, 13 | }, 14 | }; 15 | 16 | export const tabs = () => 17 | html` 18 | 19 |
foo
20 |
foo content
21 |
bar
22 |
bar content
23 |
bar
24 |
bar content
25 |
bar
26 |
bar content
27 |
28 | `; 29 | 30 | export const single = () => 31 | html` 32 | 48 | `; 49 | 50 | export const three = () => 51 | html` 52 | 68 | 83 | 99 | `; 100 | 101 | export const info = () => 102 | html` 103 | 121 | `; 122 | 123 | export const readme = () => 124 | html` 125 | 143 | `; 144 | 145 | export const demo = () => 146 | html` 147 | 165 | `; 166 | 167 | export const links = () => 168 | html` 169 | 187 | `; 188 | 189 | export const source = () => 190 | html` 191 | 209 | `; 210 | -------------------------------------------------------------------------------- /research/elasticIndex.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-wc-card$0.0.6", 3 | "name": "test-wc-card", 4 | "version": "0.0.6", 5 | "description": "Some text about", 6 | "authors": ["Thomas Allmer", "open-wc"], 7 | "homepage": "http://foo.com", 8 | "licence": "MIT", 9 | "module": true, 10 | "main": true, 11 | "dependencies": [ 12 | "lit-element$2.x.x", 13 | "lit-element$2.0.x", 14 | "lit-element$2.0.1", 15 | "lit-element$^2.0.1", 16 | "lit-html$1.x.x", 17 | "lit-html$1.0.x", 18 | "lit-html$1.0.0", 19 | "lit-html$^1.0.0" 20 | ], 21 | "customElements": ["test-wc-card", "other-card"], 22 | "attributes": ["header", "foo"], 23 | "properties": ["side"] 24 | } 25 | -------------------------------------------------------------------------------- /research/faunadb.txt: -------------------------------------------------------------------------------- 1 | # search function 2 | Map( 3 | Paginate( 4 | Filter( 5 | Match(Index("allCustomElements")), 6 | Lambda( 7 | "i", 8 | Not( 9 | Equals(FindStr(Select(["data", "name"], Get(Var("i"))), "test"), -1) 10 | ) 11 | ) 12 | ) 13 | ), 14 | Lambda("X", Get(Var("X"))) 15 | ) -------------------------------------------------------------------------------- /research/normalized/db.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { GraphQLClient } from 'graphql-request'; 3 | 4 | const endpoint = 'https://graphql.fauna.com/graphql'; 5 | const graphQLClient = new GraphQLClient(endpoint, { 6 | headers: { 7 | authorization: 'Bearer fnADYc2oY-ACAjR9EnRZT2RrGt5FifqJDpT2UM8H', 8 | }, 9 | }); 10 | 11 | /** 12 | * Object => GraphQL query 13 | * 14 | * @param {mixed} obj 15 | * @return template string. 16 | */ 17 | const queryfy = obj => { 18 | if (typeof obj === 'number') { 19 | return obj; 20 | } 21 | 22 | if (Array.isArray(obj)) { 23 | const props = obj.map(value => `${queryfy(value)}`).join(','); 24 | return `[${props}]`; 25 | } 26 | 27 | if (typeof obj === 'object') { 28 | const props = Object.keys(obj) 29 | .map(key => `${key}:${queryfy(obj[key])}`) 30 | .join(','); 31 | return `{${props}}`; 32 | } 33 | 34 | return JSON.stringify(obj); 35 | }; 36 | 37 | export async function dbHasPackage(id) { 38 | const result = await graphQLClient.request(` 39 | query { 40 | packageById(id: "${id}") { 41 | data { 42 | name 43 | } 44 | } 45 | } 46 | `); 47 | return result.packageById.data.length !== 0; 48 | } 49 | 50 | export async function dbGetPackage(id) { 51 | const result = await graphQLClient.request(` 52 | query { 53 | packageById(id: "${id}") { 54 | data { 55 | _id 56 | name 57 | description 58 | version 59 | module 60 | license 61 | customElements { 62 | data { 63 | name 64 | } 65 | } 66 | } 67 | } 68 | } 69 | `); 70 | return result.packageById.data; 71 | } 72 | 73 | export async function dbGetAllIdsForPackage(id) { 74 | const result = await graphQLClient.request(` 75 | query getAllIds { 76 | packageById(id: "${id}") { 77 | data { 78 | _id 79 | customElements { 80 | data { 81 | _id 82 | attributes { 83 | data { 84 | _id 85 | values { 86 | data { 87 | _id 88 | } 89 | } 90 | } 91 | } 92 | properties { 93 | data { 94 | _id 95 | values { 96 | data { 97 | _id 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | `); 108 | return result; 109 | } 110 | 111 | // TODO: split up this horrible giant function 112 | export async function dbAddPackage(pkg) { 113 | const customElementsCreate = []; 114 | 115 | pkg.customElements.tags.forEach(tag => { 116 | const attributesCreate = []; 117 | tag.attributes.forEach(attribute => { 118 | const newAttribute = { 119 | name: attribute.name, 120 | type: attribute.type, 121 | }; 122 | if (attribute.values) { 123 | newAttribute.values = { create: attribute.values }; 124 | } 125 | attributesCreate.push(newAttribute); 126 | }); 127 | const propertiesCreate = []; 128 | tag.properties.forEach(property => { 129 | const newProperty = { 130 | name: property.name, 131 | type: property.type, 132 | }; 133 | if (property.attribute) { 134 | newProperty.attribute = property.attribute; 135 | } 136 | if (property.reflect) { 137 | newProperty.reflect = property.reflect; 138 | } 139 | if (property.values) { 140 | newProperty.values = { create: property.values }; 141 | } 142 | propertiesCreate.push(newProperty); 143 | }); 144 | 145 | const newTag = { 146 | id: `${tag.name}$${pkg.version}`, 147 | name: tag.name, 148 | attributes: { 149 | create: attributesCreate, 150 | }, 151 | properties: { 152 | create: propertiesCreate, 153 | }, 154 | }; 155 | customElementsCreate.push(newTag); 156 | }); 157 | 158 | const data = { 159 | id: `${pkg.name}$${pkg.version}`, 160 | name: pkg.name, 161 | version: pkg.version, 162 | packageJsonString: JSON.stringify(pkg.packageJson), 163 | customElementsString: JSON.stringify(pkg.customElements), 164 | customElements: { 165 | create: customElementsCreate, 166 | }, 167 | }; 168 | if (pkg.description) { 169 | data.description = pkg.description; 170 | } 171 | if (pkg.homepage) { 172 | data.homepage = pkg.homepage; 173 | } 174 | if (pkg.license) { 175 | data.license = pkg.license; 176 | } 177 | if (pkg.module) { 178 | data.module = pkg.module; 179 | } 180 | if (pkg.main) { 181 | data.main = pkg.main; 182 | } 183 | if (pkg.size) { 184 | data.size = pkg.size; 185 | } 186 | if (pkg.sizeGzip) { 187 | data.sizeGzip = pkg.sizeGzip; 188 | } 189 | 190 | const query = ` 191 | mutation createPackage { 192 | createPackage( 193 | data: ${queryfy(data)} 194 | ) { 195 | name 196 | id 197 | _id 198 | customElements { 199 | data { 200 | id 201 | name 202 | attributes { 203 | data { 204 | name 205 | type 206 | } 207 | } 208 | properties { 209 | data { 210 | name 211 | type 212 | attribute 213 | reflect 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | `; 221 | console.log(`Adding Package ${pkg.name}@${pkg.version}`); 222 | pkg.customElements.tags.forEach(tag => { 223 | console.log(`Adding CustomElement ${tag.name}`); 224 | }); 225 | console.log('---'); 226 | 227 | try { 228 | await graphQLClient.request(query); 229 | } catch (err) { 230 | console.log('## Could not execute `createPackage` on db', err); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /research/normalized/schema.gql: -------------------------------------------------------------------------------- 1 | type Package { 2 | id: String! @unique 3 | name: String! 4 | version: String! 5 | time: String 6 | description: String 7 | homepage: String 8 | license: String 9 | hasModule: Boolean 10 | hasMain: Boolean 11 | module: String 12 | main: String 13 | size: Int 14 | sizeGzip: Int 15 | packageJsonString: String 16 | customElementsString: String 17 | customElements: [CustomElement!]! @relation 18 | } 19 | 20 | type ValueSet { 21 | name: String! 22 | attribute: Attribute 23 | property: Property 24 | } 25 | 26 | type Attribute { 27 | name: String! 28 | type: String! 29 | values: [ValueSet!] @relation 30 | customElement: CustomElement 31 | deprecated: Boolean 32 | deprecatedMessage: String 33 | } 34 | 35 | type Property { 36 | name: String! 37 | type: String! 38 | attribute: String 39 | reflect: Boolean 40 | values: [ValueSet!] @relation 41 | customElement: CustomElement 42 | deprecated: Boolean 43 | deprecatedMessage: String 44 | } 45 | 46 | type CustomElement { 47 | id: String! @unique 48 | name: String! 49 | attributes: [Attribute!]! @relation 50 | properties: [Property!]! @relation 51 | package: Package 52 | } 53 | 54 | type Query { 55 | allAttributes: [Attribute!] 56 | allProperties: [Property!] 57 | allValueSets: [ValueSet!] 58 | 59 | allPackages: [Package!] 60 | packageById(id: String!): [Package!] 61 | packageByName(name: String!): [Package!] 62 | 63 | allCustomElements: [CustomElement!] 64 | customElementById(id: String!): [CustomElement!] 65 | customElementByName(name: String!): [CustomElement!] 66 | } 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | // @ts-nocheck 3 | 4 | const { DEFAULT_EXTENSIONS } = require('@babel/core'); 5 | const { findSupportedBrowsers } = require('@open-wc/building-utils'); 6 | const resolve = require('rollup-plugin-node-resolve'); 7 | const { terser } = require('rollup-plugin-terser'); 8 | const babel = require('rollup-plugin-babel'); 9 | const indexHTML = require('rollup-plugin-index-html'); 10 | 11 | const production = !process.env.ROLLUP_WATCH; 12 | 13 | /** 14 | * @typedef {ConfigOptions} 15 | * @param {*} _options 16 | * @param {*} legacy 17 | */ 18 | 19 | module.exports = function createBasicConfig(_options) { 20 | const options = { 21 | outputDir: 'dist', 22 | extensions: DEFAULT_EXTENSIONS, 23 | indexHTMLPlugin: {}, 24 | ..._options, 25 | }; 26 | 27 | return { 28 | input: 'index.html', 29 | treeshake: !!production, 30 | output: { 31 | dir: options.outputDir, 32 | format: 'esm', 33 | sourcemap: true, 34 | dynamicImportFunction: 'importShim', 35 | }, 36 | plugins: [ 37 | // parse input index.html as input and feed any modules found to rollup 38 | indexHTML({ 39 | ...(options.indexHTMLPlugin || {}), 40 | polyfills: { 41 | ...((options.indexHTMLPlugin && options.indexHTMLPlugin.polyfills) || {}), 42 | dynamicImport: true, 43 | webcomponents: true, 44 | }, 45 | }), 46 | 47 | // resolve bare import specifiers 48 | resolve({ 49 | extensions: options.extensions, 50 | }), 51 | 52 | // run code through babel 53 | babel({ 54 | extensions: options.extensions, 55 | plugins: [ 56 | '@babel/plugin-syntax-dynamic-import', 57 | '@babel/plugin-syntax-import-meta', 58 | // rollup rewrites import.meta.url, but makes them point to the file location after bundling 59 | // we want the location before bundling 60 | 'bundled-import-meta', 61 | ].filter(_ => !!_), 62 | 63 | presets: [ 64 | [ 65 | '@babel/preset-env', 66 | { 67 | targets: findSupportedBrowsers(), 68 | // preset-env compiles template literals for safari 12 due to a small bug which 69 | // doesn't affect most use cases. for example lit-html handles it: (https://github.com/Polymer/lit-html/issues/575) 70 | exclude: ['@babel/plugin-transform-template-literals'], 71 | useBuiltIns: false, 72 | modules: false, 73 | }, 74 | ], 75 | ], 76 | }), 77 | 78 | // only minify if in production 79 | production && terser(), 80 | ], 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /schema.gql: -------------------------------------------------------------------------------- 1 | type Package { 2 | id: String! @unique 3 | name: String! 4 | version: String! 5 | description: String 6 | homepage: String 7 | license: String 8 | hasModule: Boolean 9 | hasMain: Boolean 10 | module: String 11 | main: String 12 | size: Int 13 | sizeGzip: Int 14 | githubStars: Int 15 | githubWatchers: Int 16 | githubForks: Int 17 | githubUrl: String 18 | unpkgUrl: String 19 | npmUrl: String 20 | packageJsonString: String 21 | customElementsString: String 22 | versionTime: String 23 | readme: String 24 | demoUrl: String 25 | flattenedDependencies: String 26 | } 27 | 28 | type Query { 29 | allPackages: [Package!] 30 | packageById(id: String!): [Package!] 31 | packageByName(name: String!): [Package!] 32 | } 33 | -------------------------------------------------------------------------------- /src/lambda/check-package-with-version.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-await-in-loop */ 2 | import semver from 'semver'; 3 | 4 | import { aRequest, aTimeout, githubRequest } from './helpers/helpers.js'; 5 | import { dbAddPackage, dbHasPackage, dbDeletePackage } from './helpers/db.js'; 6 | import { searchUpdatePackage } from './helpers/elastic.js'; 7 | import { npmIsValidVersion, isLatestNpmVersion } from './helpers/npm.js'; 8 | 9 | const customElementsFile = 'custom-elements.json'; 10 | 11 | function isValidPackageId(_id) { 12 | let id = _id; 13 | if (!id) { 14 | return false; 15 | } 16 | if (id[0] === '@') { 17 | // remove npm scope @ 18 | id = id.substr(1); 19 | } 20 | return id.includes('@'); 21 | } 22 | 23 | async function getSizes(name, version) { 24 | const result = await aRequest(`https://bundlephobia.com/api/size?package=${name}@${version}`); 25 | const data = JSON.parse(result); 26 | return { 27 | size: data.size, 28 | sizeGzip: data.gzip, 29 | dependencySizes: data.dependencySizes, 30 | }; 31 | } 32 | 33 | // TODO: get downloads from NPM 34 | // https://github.com/npm/registry/blob/master/docs/download-counts.md 35 | 36 | // TODO: will need to rotate Github API keys because of rate limits 37 | // potentially switch to v4 API with GraphQL (rate limits are worse so only if queried info is worth it) 38 | // https://developer.github.com/v4/explorer/ 39 | async function getGithubInfo(pkg) { 40 | if (pkg.repository && pkg.repository.url && pkg.repository.url.includes('github.com/')) { 41 | const { url } = pkg.repository; 42 | const start = url.indexOf('github.com/') + 11; 43 | const end = url.lastIndexOf('.git'); 44 | const repoName = url.substring(start, end); 45 | 46 | const data = await githubRequest(`/repos/${repoName}`); 47 | return { 48 | githubUrl: `https://github.com/${repoName}`, 49 | githubStars: data.stargazers_count, 50 | githubWatchers: data.watchers_count, 51 | githubForks: data.forks_count, 52 | githubOpenIssues: data.open_issues_count, 53 | githubLastPush: data.pushed_at, 54 | }; 55 | } 56 | return {}; 57 | } 58 | 59 | function flattenDependencies(deps) { 60 | const flattened = []; 61 | Object.keys(deps).forEach(dep => { 62 | const val = deps[dep]; 63 | const versionData = semver.minVersion(val); 64 | flattened.push(`${dep}@${versionData.major}.x.x`); 65 | flattened.push(`${dep}@${versionData.major}.${versionData.minor}.x`); 66 | flattened.push(`${dep}@${versionData.major}.${versionData.minor}.${versionData.patch}`); 67 | flattened.push(`${dep}@${val}`); 68 | }); 69 | return flattened; 70 | } 71 | 72 | async function getDemoUrl(packageJson) { 73 | const { name, version, demoUrl } = packageJson; 74 | if (demoUrl) { 75 | return demoUrl; 76 | } 77 | const potentialDemoUrl = `https://unpkg.com/${name}@${version}/demo/index.html?module`; 78 | const demoResponse = await aRequest(potentialDemoUrl); 79 | let findDemoUrl = ''; 80 | if (demoResponse !== `Cannot find "/demo/index.html" in ${name}@${version}`) { 81 | findDemoUrl = potentialDemoUrl; 82 | } 83 | return findDemoUrl; 84 | } 85 | 86 | export async function handler(ev) { 87 | const { id } = ev.queryStringParameters; 88 | 89 | if (!isValidPackageId(id)) { 90 | return { 91 | statusCode: 200, 92 | body: `"${id}" is not valid. Format is like "my-npm-package@1.0.0" or "my-scope@my-npm-package@1.0.0"`, 93 | }; 94 | } 95 | 96 | const splitIndex = id.lastIndexOf('@'); 97 | const name = id.substr(0, splitIndex); 98 | const version = id.substr(splitIndex + 1); 99 | const ceFileUrl = `https://unpkg.com/${name}@${version}/${customElementsFile}`; 100 | const npmMetaDataUrl = `https://registry.npmjs.org/${name}`; 101 | const packageMetaDataString = await aRequest(npmMetaDataUrl); 102 | const packageMetaData = JSON.parse(packageMetaDataString); 103 | 104 | if (!npmIsValidVersion(packageMetaData, version)) { 105 | return { 106 | statusCode: 200, 107 | body: `Version ${version} for "${name}" could not be found. Are you sure it is published?`, 108 | }; 109 | } 110 | 111 | let unpkgResponse; 112 | // if unpkg does not have the package yet then wait/fetch with increasing intervales 113 | try { 114 | const timeouts = [10, 100, 1000, 2000]; 115 | unpkgResponse = await aRequest(ceFileUrl); 116 | if (unpkgResponse === `Cannot find package ${name}@${version}`) { 117 | let i = 0; 118 | do { 119 | unpkgResponse = await aRequest(ceFileUrl); 120 | await aTimeout(timeouts[i]); 121 | i += 1; 122 | } while (unpkgResponse === `Cannot find package ${name}@${version}` && i < timeouts.length); 123 | } 124 | if (unpkgResponse === `Cannot find package ${name}@${version}`) { 125 | return { 126 | statusCode: 200, 127 | body: `Package "${name}@${version}" was not found. Is it published? We checked via https://unpkg.com/${name}@${version}/`, 128 | }; 129 | } 130 | } catch (err) { 131 | return { 132 | statusCode: 200, 133 | body: `We could not reach unpkg.com. Please try again later.`, 134 | }; 135 | } 136 | 137 | if (unpkgResponse === `Cannot find "/${customElementsFile}" in ${name}@${version}`) { 138 | return { 139 | statusCode: 200, 140 | body: `Package "${name}@${version}" does not have a ${customElementsFile} and can therefore not be added to the web components catalog.`, 141 | }; 142 | } 143 | 144 | if (unpkgResponse[0] === '{') { 145 | const packageJson = packageMetaData.versions[version]; 146 | const customElements = JSON.parse(unpkgResponse); 147 | let body = `The package "${id}" got processed:`; 148 | 149 | const sizes = await getSizes(name, version); 150 | const githubInfo = await getGithubInfo(packageJson); 151 | const readme = await aRequest(`https://unpkg.com/${name}@${version}/README.md`); 152 | const demoUrl = await getDemoUrl(packageJson); 153 | 154 | const pkg = { 155 | ...packageJson, 156 | ...sizes, 157 | ...githubInfo, 158 | readme, 159 | demoUrl, 160 | versionTime: packageMetaData.time[version], 161 | customElements, 162 | packageJson, 163 | // TODO: gather nested dependencies instead of using packageJson.dependencies 164 | // mimic https://github.com/npm/npm-remote-ls/blob/master/lib/remote-ls.js without all the heavy dependencies 165 | flattenedDependencies: flattenDependencies( 166 | packageJson.dependencies ? packageJson.dependencies : {}, 167 | ), 168 | }; 169 | 170 | if (!(await dbHasPackage(id))) { 171 | await dbAddPackage(pkg); 172 | body += `- added to database`; 173 | } else { 174 | await dbDeletePackage(id); 175 | await dbAddPackage(pkg); 176 | body += `- updated in database`; 177 | } 178 | 179 | // TODO: better heuristic? check for version in elastic if lower then updated 180 | // even if not latest... is that better? 181 | if (isLatestNpmVersion(packageMetaData, version)) { 182 | await searchUpdatePackage(pkg); 183 | body += `- updated search index (as this is the latest version)`; 184 | } 185 | 186 | return { 187 | statusCode: 200, 188 | body, 189 | }; 190 | } 191 | 192 | return { 193 | statusCode: 200, 194 | body: `Unknown error - something went seriously wrong :/ please file an issue.`, 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /src/lambda/helpers/db.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { GraphQLClient } from 'graphql-request'; 3 | 4 | const endpoint = 'https://graphql.fauna.com/graphql'; 5 | 6 | // TODO: use just fetch 7 | const graphQLClient = new GraphQLClient(endpoint, { 8 | headers: { 9 | // Key Name/ID: Public Full Access 10 | authorization: 'Bearer fnADZViyjRACCwKie9EtJDoUOW2TCzFn0FzR5mH1', 11 | }, 12 | }); 13 | 14 | /** 15 | * Object => GraphQL query 16 | * 17 | * @param {mixed} obj 18 | * @return template string. 19 | */ 20 | const queryfy = obj => { 21 | if (typeof obj === 'number') { 22 | return obj; 23 | } 24 | 25 | if (Array.isArray(obj)) { 26 | const props = obj.map(value => `${queryfy(value)}`).join(','); 27 | return `[${props}]`; 28 | } 29 | 30 | if (typeof obj === 'object') { 31 | const props = Object.keys(obj) 32 | .map(key => `${key}:${queryfy(obj[key])}`) 33 | .join(','); 34 | return `{${props}}`; 35 | } 36 | 37 | return JSON.stringify(obj); 38 | }; 39 | 40 | export async function dbHasPackage(id) { 41 | const result = await graphQLClient.request(` 42 | query { 43 | packageById(id: "${id}") { 44 | data { 45 | _id 46 | } 47 | } 48 | } 49 | `); 50 | return result.packageById.data.length !== 0; 51 | } 52 | 53 | export async function dbGetPackage(id) { 54 | const result = await graphQLClient.request(` 55 | query { 56 | packageById(id: "${id}") { 57 | data { 58 | _id 59 | name 60 | description 61 | version 62 | module 63 | license 64 | size 65 | sizeGzip 66 | packageJsonString 67 | customElementsString 68 | githubStars 69 | githubWatchers 70 | githubForks 71 | githubUrl 72 | unpkgUrl 73 | npmUrl 74 | versionTime 75 | readme 76 | demoUrl 77 | flattenedDependencies 78 | } 79 | } 80 | } 81 | `); 82 | return result.packageById.data[0]; 83 | } 84 | 85 | export async function dbDeletePackage(id) { 86 | const packageInfo = await dbGetPackage(id); 87 | await graphQLClient.request(` 88 | mutation { 89 | deletePackage(id: "${packageInfo._id}") { 90 | _id 91 | } 92 | } 93 | `); 94 | } 95 | 96 | export async function dbAddPackage(pkg) { 97 | const data = { 98 | id: `${pkg.name}@${pkg.version}`, 99 | name: pkg.name, 100 | version: pkg.version, 101 | packageJsonString: JSON.stringify(pkg.packageJson), 102 | customElementsString: JSON.stringify(pkg.customElements), 103 | unpkgUrl: `https://unpkg.com/${pkg.name}@${pkg.version}/`, 104 | npmUrl: `https://www.npmjs.com/package/${pkg.name}/v/${pkg.version}`, 105 | flattenedDependencies: JSON.stringify(pkg.flattenedDependencies), 106 | }; 107 | const addIfFound = [ 108 | 'description', 109 | 'homepage', 110 | 'licence', 111 | 'module', 112 | 'main', 113 | 'size', 114 | 'sizeGzip', 115 | 'githubStars', 116 | 'githubWatchers', 117 | 'githubForks', 118 | 'githubUrl', 119 | 'versionTime', 120 | 'readme', 121 | 'demoUrl', 122 | ]; 123 | addIfFound.forEach(item => { 124 | if (pkg[item] !== undefined) { 125 | data[item] = pkg[item]; 126 | } 127 | }); 128 | 129 | const query = ` 130 | mutation createPackage { 131 | createPackage( 132 | data: ${queryfy(data)} 133 | ) { 134 | _id 135 | } 136 | } 137 | `; 138 | 139 | try { 140 | await graphQLClient.request(query); 141 | } catch (err) { 142 | console.log('## Could not execute `createPackage` on db', err); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/lambda/helpers/elastic.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-await-in-loop */ 2 | 3 | import fetch from 'node-fetch'; 4 | 5 | // // playground login 6 | // const username = '6VWygp4joU'; 7 | // const password = '9NBHqCyR6EVxhWLu7tArK53F'; 8 | 9 | function elastic(_url, method = 'GET', body = null) { 10 | const url = `https://search-catalog-testing-xty6jjvifguawmpk2f7iop3b2a.us-east-2.es.amazonaws.com${_url}`; 11 | const headers = { 12 | 'Content-Type': 'application/json', 13 | // Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, 14 | }; 15 | const options = { method, headers }; 16 | 17 | if (body) { 18 | options.body = JSON.stringify(body); 19 | } 20 | 21 | return new Promise((resolve, reject) => { 22 | fetch(url, options) 23 | .then(res => res.json()) 24 | .then(json => { 25 | resolve(json); 26 | }) 27 | .catch(err => { 28 | console.error(JSON.stringify(err, null, 2)); 29 | reject(); 30 | }); 31 | }); 32 | } 33 | 34 | function elasticPut(_url, body) { 35 | return elastic(_url, 'PUT', body); 36 | } 37 | 38 | function elasticPost(_url, body) { 39 | return elastic(_url, 'POST', body); 40 | } 41 | 42 | function parseV2CustomElements(customElements) { 43 | const data = { 44 | tags: [], 45 | }; 46 | 47 | customElements.tags.forEach(tag => { 48 | data.tags.push(tag.name); 49 | 50 | if (tag.attributes) { 51 | data.attributes = tag.attributes.reduce((acc, attr) => [...acc, attr.name], []); 52 | } 53 | if (tag.properties) { 54 | data.properties = tag.properties.reduce((acc, attr) => [...acc, attr.name], []); 55 | } 56 | }); 57 | 58 | return data; 59 | } 60 | 61 | function createFlatSearchablePackage(pkg) { 62 | const flatPkg = { 63 | id: `${pkg.name}@${pkg.version}`, 64 | name: pkg.name, 65 | version: pkg.version, 66 | module: !!pkg.module, 67 | main: !!pkg.main, 68 | hasDemo: !!pkg.demoUrl, 69 | }; 70 | if (pkg.customElements && pkg.customElements.version === 2) { 71 | Object.assign(flatPkg, parseV2CustomElements(pkg.customElements)); 72 | } 73 | 74 | const addIfFound = [ 75 | 'description', 76 | 'homepage', 77 | 'licence', 78 | 'size', 79 | 'sizeGzip', 80 | 'githubStars', 81 | 'githubWatchers', 82 | 'githubForks', 83 | 'versionTime', 84 | 'readme', 85 | 'flattenedDependencies', 86 | ]; 87 | addIfFound.forEach(item => { 88 | if (pkg[item] !== undefined) { 89 | flatPkg[item] = pkg[item]; 90 | } 91 | }); 92 | 93 | return flatPkg; 94 | } 95 | 96 | export async function searchUpdatePackage(pkg) { 97 | const searchablePkg = createFlatSearchablePackage(pkg); 98 | 99 | const result = await elasticPut(`/packages/_doc/${pkg.name}`, searchablePkg); 100 | if (result && result.error) { 101 | console.error(result.error); 102 | throw new Error('Could not index file'); 103 | } 104 | return result; 105 | } 106 | 107 | export async function search(queryString) { 108 | const result = await elasticPost('/packages/_search', { 109 | query: { 110 | multi_match: { 111 | query: queryString, 112 | fields: ['name^4', 'description^2', 'homepage', 'tags', 'attributes', 'properties'], 113 | fuzziness: 3, 114 | prefix_length: 1, 115 | }, 116 | }, 117 | }); 118 | return result; 119 | } 120 | 121 | export async function searchFilterDependencies(queryString, dependencies) { 122 | const should = dependencies.map(dep => ({ 123 | match_phrase: { 124 | flattenedDependencies: { 125 | query: dep, 126 | }, 127 | }, 128 | })); 129 | 130 | const filter = { 131 | bool: { 132 | should, 133 | }, 134 | }; 135 | 136 | const result = await elasticPost('/packages/_search', { 137 | query: { 138 | bool: { 139 | must: { 140 | multi_match: { 141 | query: queryString, 142 | fields: ['name^4', 'description^2', 'homepage', 'tags', 'attributes', 'properties'], 143 | fuzziness: 3, 144 | prefix_length: 1, 145 | }, 146 | }, 147 | filter, 148 | }, 149 | }, 150 | }); 151 | return result; 152 | } 153 | -------------------------------------------------------------------------------- /src/lambda/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import https from 'https'; 3 | 4 | // TODO: use fetch 5 | export const aRequest = url => 6 | new Promise((resolve, reject) => { 7 | https 8 | .get(url, resp => { 9 | let data = ''; 10 | resp.on('data', chunk => { 11 | data += chunk; 12 | }); 13 | 14 | resp.on('end', () => { 15 | resolve(data); 16 | }); 17 | }) 18 | .on('error', err => { 19 | reject(err.message); 20 | }); 21 | }); 22 | 23 | export function aTimeout(ms) { 24 | return new Promise(resolve => { 25 | setTimeout(resolve, ms); 26 | }); 27 | } 28 | 29 | export function githubRequest(_url) { 30 | const url = `https://api.github.com${_url}`; 31 | const headers = { 32 | 'User-Agent': 'open-wc calendar', 33 | }; 34 | const options = { method: 'GET', headers }; 35 | 36 | return new Promise((resolve, reject) => { 37 | fetch(url, options) 38 | .then(res => res.json()) 39 | .then(json => { 40 | resolve(json); 41 | }) 42 | .catch(err => { 43 | // eslint-disable-next-line no-console 44 | console.error(JSON.stringify(err, null, 2)); 45 | reject(); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/lambda/helpers/npm.js: -------------------------------------------------------------------------------- 1 | export function npmIsValidVersion(meta, version) { 2 | return meta && meta.versions && meta.versions[version]; 3 | } 4 | 5 | export function npmGetLatestVersion(meta) { 6 | const versions = Object.keys(meta.versions); 7 | return versions[versions.length - 1]; 8 | } 9 | 10 | export function isLatestNpmVersion(meta, version) { 11 | return npmGetLatestVersion(meta) === version; 12 | } 13 | -------------------------------------------------------------------------------- /src/lambda/search.js: -------------------------------------------------------------------------------- 1 | import { search, searchFilterDependencies } from './helpers/elastic.js'; 2 | import { dbGetPackage } from './helpers/db.js'; 3 | 4 | export async function handler(ev) { 5 | const { queryString, dependencies } = ev.queryStringParameters; 6 | 7 | // this normalizes the array handling of queryStringParameters 8 | // locally I get "foo" and ["foo", "bar"] 9 | // on netlify I get "foo" and "foo, bar" 10 | // TODO: move to separate function 11 | let dependenciesArray = dependencies; 12 | if (dependencies && !Array.isArray(dependencies)) { 13 | if (dependencies.includes(',')) { 14 | dependenciesArray = dependencies.split(','); 15 | } else { 16 | dependenciesArray = [dependencies]; 17 | } 18 | } 19 | 20 | // 1. search in elasticsearch 21 | const result = dependencies 22 | ? await searchFilterDependencies(queryString, dependenciesArray) 23 | : await search(queryString); 24 | 25 | if (result.hits.total.value === 0) { 26 | return { 27 | statusCode: 200, 28 | body: JSON.stringify({ 'no-result': true }, null, 2), 29 | }; 30 | } 31 | 32 | // 2. query faunadb for all returned elasticsearch ids 33 | const dbRequests = []; 34 | result.hits.hits.forEach(hit => { 35 | dbRequests.push(dbGetPackage(hit._source.id)); 36 | }); 37 | const filled = await Promise.all(dbRequests); 38 | 39 | // preprocess json encoded flattenDependencies 40 | // => do not want to add more collections to faunadb to keep it simple for now 41 | filled.forEach((pkg, i) => { 42 | filled[i].flattenedDependencies = JSON.parse(pkg.flattenedDependencies); 43 | }); 44 | 45 | // 3. return db filled data 46 | return { 47 | statusCode: 200, 48 | body: JSON.stringify(filled, null, 2), 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - Current user/passwords are hard coded with write rights (to allow for easy playing around) before publish revoke those users and use netlify secretes 4 | --------------------------------------------------------------------------------