├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── gh-pages.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.config.js ├── cypress ├── e2e │ └── single-model │ │ └── single-model.spec.js ├── fixtures │ └── single-model │ │ ├── .discoveryrc.cjs │ │ └── ui │ │ └── page │ │ └── default.js ├── plugins │ └── index.сjs ├── support │ ├── commands.js │ └── e2e.js └── test.sh ├── dist ├── .gitignore └── .npmignore ├── docs ├── embed.md ├── encodings.md ├── load-data.md └── upload.md ├── gh-pages.config.cjs ├── gh-pages.extensions.js ├── models ├── embed.html ├── index.cjs └── load-benchmark │ ├── index.cjs │ ├── ui.css │ └── ui.js ├── package-lock.json ├── package.json ├── scripts └── transpile.js ├── src ├── core │ ├── action.ts │ ├── color-scheme.ts │ ├── dict.ts │ ├── emitter.ts │ ├── encodings │ │ ├── index.ts │ │ ├── json.ts │ │ ├── jsonxl-snapshot9.js │ │ ├── jsonxl-snapshot9.ts │ │ ├── jsonxl.ts │ │ └── utils.ts │ ├── object-marker.ts │ ├── observer.ts │ ├── page.ts │ ├── preset.ts │ ├── text-view.ts │ ├── utils │ │ ├── base64.ts │ │ ├── compare.ts │ │ ├── container-styles.ts │ │ ├── copy-text.ts │ │ ├── debounce.ts │ │ ├── dom.ts │ │ ├── html.ts │ │ ├── id.ts │ │ ├── index-script.ts │ │ ├── index.ts │ │ ├── inject-styles.ts │ │ ├── is-type.ts │ │ ├── json.ts │ │ ├── layout.ts │ │ ├── load-data-streams.ts │ │ ├── load-data.ts │ │ ├── load-data.types.ts │ │ ├── location-sync.ts │ │ ├── logger.ts │ │ ├── object-utils.ts │ │ ├── pattern.ts │ │ ├── persistent.ts │ │ ├── pointer.ts │ │ ├── progressbar.ts │ │ ├── query-to-config.ts │ │ ├── safe-filter-rx.ts │ │ └── size.ts │ └── view.ts ├── extensions │ ├── embed-client.ts │ ├── embed-host.ts │ ├── embed-message.types.ts │ ├── inspector.css │ ├── inspector.js │ ├── inspector │ │ ├── data.js │ │ ├── overlay.css │ │ ├── popup-content.css │ │ ├── popup-sidebar.css │ │ ├── popup-toolbar.css │ │ ├── popup.css │ │ ├── props-config.js │ │ ├── utils.js │ │ └── view-tree.js │ ├── modelfree.ts │ ├── router.ts │ └── upload.ts ├── favicon.png ├── lib-script.ts ├── lib.css ├── lib.ts ├── logo.svg ├── main │ ├── app.css │ ├── app.ts │ ├── index.css │ ├── index.ts │ ├── model-extension-api.ts │ ├── model-legacy-extension-api.ts │ ├── model.ts │ ├── query-suggestions.ts │ ├── style │ │ ├── base.css │ │ └── sidebar.css │ ├── view-model.css │ └── view-model.ts ├── nav │ ├── buttons.ts │ ├── img │ │ ├── burger-menu.svg │ │ ├── clipboard.png │ │ ├── github.svg │ │ └── inspect.svg │ ├── index.css │ └── index.ts ├── pages │ ├── default.css │ ├── default.js │ ├── discovery.css │ ├── discovery.js │ ├── discovery │ │ ├── editor-common.css │ │ ├── editor-query.css │ │ ├── editor-query.ts │ │ ├── editor-view.css │ │ ├── editor-view.ts │ │ ├── header.css │ │ ├── header.ts │ │ ├── img │ │ │ ├── clone-root.svg │ │ │ ├── clone.svg │ │ │ ├── delete.svg │ │ │ ├── edit.svg │ │ │ ├── expand.svg │ │ │ ├── formatting.svg │ │ │ ├── fullscreen-off.svg │ │ │ ├── fullscreen-on.svg │ │ │ ├── minus.svg │ │ │ ├── perform.svg │ │ │ ├── plus.svg │ │ │ ├── share.svg │ │ │ ├── stash-root.svg │ │ │ ├── stash.svg │ │ │ ├── subquery.svg │ │ │ └── suggestions.svg │ │ ├── params.ts │ │ └── types.ts │ ├── index.css │ ├── index.js │ ├── not-found.js │ ├── views-showcase.css │ ├── views-showcase.js │ └── views-showcase │ │ ├── intro-text-render-md.js │ │ ├── intro-web-render-md.js │ │ ├── view-usage-render.css │ │ └── view-usage-render.ts ├── preloader.css ├── preloader.ts ├── text-views │ ├── alert.js │ ├── alert.usage.js │ ├── badge.js │ ├── badges.usage.js │ ├── base-blocks.js │ ├── base-blocks.usage.js │ ├── blockquote.js │ ├── blockquote.usage.js │ ├── context.js │ ├── context.usage.js │ ├── headers.js │ ├── headers.usage.js │ ├── index.js │ ├── link.js │ ├── link.usage.js │ ├── lists.js │ ├── lists.usage.js │ ├── source.js │ ├── source.usage.js │ ├── switch.js │ ├── switch.usage.js │ ├── table.js │ ├── table.usage.js │ ├── text.js │ └── text.usage.js ├── version.ts └── views │ ├── button.css │ ├── charts │ ├── histogram.css │ ├── histogram.js │ └── histogram.usage.js │ ├── context.js │ ├── context.usage.js │ ├── controls │ ├── button.css │ ├── button.js │ ├── button.usage.js │ ├── checkbox-list.css │ ├── checkbox-list.js │ ├── checkbox-list.usage.js │ ├── checkbox.css │ ├── checkbox.js │ ├── checkbox.svg │ ├── checkbox.usage.js │ ├── content-filter.css │ ├── content-filter.js │ ├── content-filter.svg │ ├── content-filter.usage.js │ ├── dropdown.css │ ├── dropdown.js │ ├── dropdown.usage.js │ ├── input.css │ ├── input.js │ ├── input.usage.js │ ├── menu-item.css │ ├── menu-item.js │ ├── menu-item.usage.js │ ├── menu.css │ ├── menu.js │ ├── menu.usage.js │ ├── nav-button.css │ ├── nav-button.js │ ├── nav-button.usage.js │ ├── progress.css │ ├── progress.js │ ├── progress.usage.js │ ├── select-arrow.svg │ ├── select.css │ ├── select.js │ ├── select.usage.js │ ├── tab.css │ ├── tab.js │ ├── tab.usage.js │ ├── tabs.css │ ├── tabs.js │ ├── tabs.usage.js │ ├── toggle-group.css │ ├── toggle-group.js │ ├── toggle-group.usage.js │ ├── toggle.css │ └── toggle.js │ ├── editor │ ├── editor-mode-query.js │ ├── editor-mode-view.js │ ├── editors-hint.css │ ├── editors-hint.js │ ├── editors.css │ └── editors.ts │ ├── index.css │ ├── index.js │ ├── layout │ ├── block.js │ ├── block.usage.js │ ├── column.css │ ├── column.js │ ├── columns.css │ ├── columns.js │ ├── columns.usage.js │ ├── expand.css │ ├── expand.js │ ├── expand.svg │ ├── expand.usage.js │ ├── hstack.css │ ├── hstack.js │ ├── hstack.usage.js │ ├── list-item.css │ ├── list-item.js │ ├── lists.css │ ├── lists.js │ ├── lists.usage.js │ ├── page-header.css │ ├── page-header.js │ ├── page-header.usage.js │ ├── popup.css │ ├── popup.ts │ ├── section.css │ ├── section.js │ ├── section.usage.js │ ├── toc-section.css │ └── toc-section.js │ ├── signature │ ├── collect-stat.js │ ├── const.js │ ├── index.js │ ├── render-details.js │ ├── render-stat.js │ ├── signature.usage.js │ └── style │ │ ├── action-button.css │ │ ├── details-popup.css │ │ ├── img │ │ └── group.svg │ │ └── index.css │ ├── struct │ ├── click-handler.js │ ├── el-proto.ts │ ├── index.js │ ├── popup-signature.js │ ├── popup-value-actions.js │ ├── render-annotations.ts │ ├── struct.usage.js │ ├── style │ │ ├── action-button.css │ │ ├── annotation.css │ │ ├── index.css │ │ └── structure.css │ └── value-to-html.ts │ ├── switch.js │ ├── switch.usage.js │ ├── table │ ├── table-cell-details-expand.svg │ ├── table-cell-utils.js │ ├── table-cell.css │ ├── table-cell.js │ ├── table-footer-cell.css │ ├── table-footer-cell.js │ ├── table-footer.css │ ├── table-footer.js │ ├── table-header-cell.css │ ├── table-header-cell.js │ ├── table-header.css │ ├── table-header.js │ ├── table-row.css │ ├── table-row.js │ ├── table-sort-asc.svg │ ├── table-sort-desc.svg │ ├── table-sortable.svg │ ├── table.css │ ├── table.js │ └── table.usage.js │ ├── text-render.css │ ├── text-render.js │ ├── text-render.usage.js │ ├── text │ ├── alerts.css │ ├── alerts.js │ ├── alerts.usage.js │ ├── app-header.css │ ├── app-header.js │ ├── app-header.usage.js │ ├── auto-link.js │ ├── badges.css │ ├── badges.js │ ├── badges.usage.js │ ├── blockquote-caution.svg │ ├── blockquote-important.svg │ ├── blockquote-note.svg │ ├── blockquote-tip.svg │ ├── blockquote-warning.svg │ ├── blockquote.css │ ├── blockquote.js │ ├── blockquote.usage.js │ ├── headers-anchor.svg │ ├── headers.css │ ├── headers.js │ ├── headers.usage.js │ ├── html.js │ ├── html.usage.js │ ├── image-preview.css │ ├── image-preview.js │ ├── image-preview.usage.js │ ├── image.css │ ├── image.js │ ├── image.svg │ ├── image.usage.js │ ├── indicator.css │ ├── indicator.js │ ├── indicator.usage.js │ ├── link.css │ ├── link.js │ ├── link.usage.js │ ├── markdown-marked-renderer.ts │ ├── markdown.css │ ├── markdown.js │ ├── markdown.usage.js │ ├── source-copied.svg │ ├── source-copy.svg │ ├── source.css │ ├── source.js │ ├── source.usage.js │ ├── text-match.css │ ├── text-match.js │ ├── text-match.usage.js │ ├── text-numeric.css │ ├── text-numeric.js │ ├── text-numeric.usage.js │ ├── text.js │ └── text.usage.js │ └── tree │ ├── tree-leaf.css │ ├── tree-leaf.js │ ├── tree.css │ ├── tree.js │ └── tree.usage.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: discoveryjs 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GH-pages 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | # This workflow contains a single job called "build" 11 | build: 12 | # The type of runner that the job will run on 13 | runs-on: ubuntu-latest 14 | 15 | # Steps represent a sequence of tasks that will be executed as part of the job 16 | steps: 17 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '20.x' 23 | - run: npm ci 24 | - run: npm run build-gh-pages 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./.gh-pages 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test changes 2 | on: 3 | push: 4 | pull_request: 5 | 6 | env: 7 | PRIMARY_NODEJS_VERSION: 22 8 | 9 | jobs: 10 | cypress-run: 11 | name: Cypress run 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 19 | cache: "npm" 20 | 21 | - run: npm ci 22 | - run: npm run transpile 23 | 24 | - name: Cypress run 25 | uses: cypress-io/github-action@v6 26 | with: 27 | start: npm run cypress:server 28 | wait-on: 'http://localhost:8124' 29 | record: true 30 | env: 31 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | lint: 35 | name: Lint 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 43 | cache: "npm" 44 | - run: npm ci 45 | - run: npm run lint 46 | 47 | typecheck: 48 | name: TypeScript typings 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 56 | cache: "npm" 57 | - run: npm ci 58 | - run: npm run typecheck 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gh-pages 2 | /node_modules/ 3 | cypress/screenshots 4 | cypress/videos 5 | tmp/ 6 | lib/ 7 | build/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2025 Roman Dvornov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | projectId: 't2rgkz', 5 | includeShadowDom: true, 6 | e2e: { 7 | // We've imported your old cypress plugins here. 8 | // You may want to clean this up later by importing these. 9 | // setupNodeEvents(on, config) { 10 | // return require('./cypress/plugins/index.сjs')(on, config) 11 | // }, 12 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}' 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /cypress/e2e/single-model/single-model.spec.js: -------------------------------------------------------------------------------- 1 | describe('Single model', () => { 2 | it('Has model name', () => { 3 | cy.visit('localhost:8124'); 4 | cy.contains('Single model'); 5 | }); 6 | 7 | it('Opens report page', () => { 8 | cy.visit('localhost:8124'); 9 | cy.contains('Discover').click(); 10 | cy.get('input').should('have.attr', 'placeholder', 'Untitled discovery'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /cypress/fixtures/single-model/.discoveryrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Single model', 3 | data: () => ({ hello: 'world' }), 4 | view: { 5 | basedir: __dirname + '/ui', 6 | assets: [ 7 | 'page/default.js' 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /cypress/fixtures/single-model/ui/page/default.js: -------------------------------------------------------------------------------- 1 | discovery.page.define('default', [ 2 | 'h1:#.model.name', 3 | 'struct' 4 | ]); 5 | -------------------------------------------------------------------------------- /cypress/plugins/index.сjs: -------------------------------------------------------------------------------- 1 | // 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (/* on, config */) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run transpile 3 | npm run cypress:server & 4 | PID=$! 5 | cypress run --headless 6 | kill $PID 7 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !.npmignore 4 | -------------------------------------------------------------------------------- /dist/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !discovery-embed-host.js 3 | !discovery-embed-host.d.ts 4 | # !discovery-embed-host.js.map 5 | !discovery-preloader.css 6 | !discovery-preloader.js 7 | # !discovery-preloader.js.map 8 | !discovery.css 9 | !discovery.js 10 | # !discovery.js.map 11 | -------------------------------------------------------------------------------- /docs/encodings.md: -------------------------------------------------------------------------------- 1 | # Encodings 2 | 3 | Discovery.js allows the configuration of custom encodings in addition to the default ones. Encodings are utilized during payload loading to decode the payload into a JavaScript object. An encoding configuration is defined by an object with these properties: 4 | 5 | - `name`: A unique string identifier for the encoding. 6 | - `test`: A function that receives the first chunk of the payload and a `done` flag (indicating whether more payload is expected or if it's complete) and returns a boolean to indicate if the encoding is applicable. 7 | - `streaming` (optional, defaults to `false`): Determines if the encoding supports streaming processing. 8 | - `decode`: A function that decodes the payload into a JavaScript value. It accepts an async iterator if `streaming` is `true`, or `Uint8Array` otherwise. 9 | 10 | The TypeScript type definition for an encoding is as follows: 11 | 12 | ```ts 13 | type Encoding = { 14 | name: string; 15 | test(chunk: Uint8Array): boolean; 16 | } & ({ 17 | streaming: true; 18 | decode(iterator: AsyncIterableIterator): Promise; 19 | } | { 20 | streaming: false; 21 | decode(payload: Uint8Array): any; 22 | }); 23 | ``` 24 | 25 | Encodings can be set using the `encodings` option in `Model`, `Widget` and `App` configurations. The [Loading Data API](./load-data.md) applies the specified encodings to the data payload, and `Model` (base class for `App` and `Widget`) integrates these encodings into its `loadData*` method calls if they are specified upon initialization. 26 | 27 | In addition to `App` and `Widget`, preloaders can pass the `encodings` configuration to a data loader if specified in `loadDataOptions`. 28 | 29 | Custom encodings are applied before the default encodings, which are, in order of application: 30 | 31 | - `jsonxl` (`snapshot9`), non-streaming 32 | - `json` (utilizing `@discoveryjs/json-ext`), streaming 33 | 34 | Example of a custom encoding: 35 | 36 | ```js 37 | new App({ 38 | encodings: [ 39 | { 40 | name: 'lines/counter', 41 | test: () => true, // Always applicable 42 | streaming: false, 43 | decode: (payload) => new TextDecoder().decode(payload).split('\n').length 44 | } 45 | ] 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /gh-pages.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Discovery.js', 3 | version: require('./package.json').version, 4 | description: 'Explore, analyze, share', 5 | icon: './src/logo.svg', 6 | view: { 7 | assets: [ 8 | './gh-pages.extensions.js' 9 | ] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /gh-pages.extensions.js: -------------------------------------------------------------------------------- 1 | import modelfree from './src/extensions/modelfree.js'; 2 | import upload from './src/extensions/upload.js'; 3 | import * as navButtons from './src/nav/buttons.js'; 4 | 5 | discovery.apply([ 6 | modelfree, 7 | upload.setup({ clipboard: true }), 8 | navButtons.uploadFile, 9 | navButtons.uploadFromClipboard, 10 | navButtons.unloadData 11 | ]); 12 | 13 | discovery.nav.primary.append({ 14 | name: 'github', 15 | text: '', 16 | href: 'https://github.com/discoveryjs/discovery', 17 | external: true 18 | }); 19 | -------------------------------------------------------------------------------- /models/index.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Discovery examples', 3 | models: { 4 | discovery: '../gh-pages.config.cjs', 5 | 'load-benchmark': './load-benchmark/index.cjs' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /models/load-benchmark/index.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Data load benchmark', 3 | view: { 4 | basedir: __dirname, 5 | assets: [ 6 | 'ui.css', 7 | 'ui.js' 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/core/action.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import { Dictionary } from './dict.js'; 4 | 5 | type Action = { 6 | name: string; 7 | callback: (...args: unknown[]) => unknown; 8 | } 9 | 10 | export class ActionManager extends Dictionary { 11 | #actionMap: Readonly>; 12 | 13 | constructor() { 14 | super(true); 15 | 16 | this.#actionMap = Object.create(null); 17 | } 18 | 19 | define(name: string, callback: Action['callback']) { 20 | if (typeof callback !== 'function') { 21 | throw new Error('callback is not a function'); 22 | } 23 | 24 | this.#actionMap = Object.freeze({ 25 | ...this.#actionMap, 26 | [name]: callback 27 | }); 28 | 29 | return ActionManager.define(this, name, Object.freeze({ 30 | name, 31 | callback 32 | })); 33 | } 34 | 35 | revoke(name: string) { 36 | if (this.has(name)) { 37 | const map = { ...this.#actionMap }; 38 | delete map[name]; 39 | this.#actionMap = Object.freeze(map); 40 | } 41 | 42 | super.revoke(name); 43 | } 44 | 45 | get actionMap() { 46 | return this.#actionMap; 47 | } 48 | 49 | call(name: string, ...args: unknown[]) { 50 | const action = this.get(name); 51 | 52 | if (action === undefined) { 53 | throw new Error(`action "${name}" doesn't exist`); 54 | } 55 | 56 | const { callback } = action; 57 | 58 | return callback(...args); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core/dict.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import { Emitter } from './emitter.js'; 4 | 5 | export class Dictionary extends Emitter<{ 6 | define: [key: K, value: V]; 7 | revoke: [key: K]; 8 | }> { 9 | #entries: Map; 10 | #allowRevoke: boolean; 11 | 12 | protected static define(dict: Dictionary, key: K, value: Readonly) { 13 | dict.#entries.set(key, value); 14 | dict.emit('define', key, value); 15 | 16 | return value; 17 | } 18 | 19 | constructor(allowRevoke?: boolean) { 20 | super(); 21 | 22 | this.#entries = new Map(); 23 | this.#allowRevoke = Boolean(allowRevoke); 24 | } 25 | 26 | revoke(key: K) { 27 | if (!this.#allowRevoke) { 28 | throw new Error('Entry revoking is not allowed'); 29 | } 30 | 31 | this.#entries.delete(key); 32 | this.emit('revoke', key); 33 | } 34 | 35 | isDefined(key: K) { 36 | return this.#entries.has(key); 37 | } 38 | has(key: K) { 39 | return this.#entries.has(key); 40 | } 41 | 42 | get(key: K) { 43 | return this.#entries.get(key); 44 | } 45 | get names() { 46 | return [...this.#entries.keys()]; 47 | } 48 | 49 | get keys() { 50 | return this.#entries.keys(); 51 | } 52 | get values() { 53 | return this.#entries.values(); 54 | } 55 | get entries() { 56 | return this.#entries.entries(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/encodings/index.ts: -------------------------------------------------------------------------------- 1 | export { encoding as json } from './json.js'; 2 | export { encoding as jsonxl } from './jsonxl.js'; 3 | -------------------------------------------------------------------------------- /src/core/encodings/json.ts: -------------------------------------------------------------------------------- 1 | import type { Encoding } from '../utils/load-data.js'; 2 | import { parseChunked } from '@discoveryjs/json-ext'; 3 | 4 | export const encoding = /* @__PURE__ */ Object.freeze({ 5 | name: 'json', 6 | test: () => true, 7 | streaming: true, 8 | decode: parseChunked 9 | }) satisfies Encoding as Encoding; 10 | -------------------------------------------------------------------------------- /src/core/encodings/jsonxl-snapshot9.ts: -------------------------------------------------------------------------------- 1 | export { decode, isHeaderAcceptable }; 2 | declare function decode(data: Uint8Array): any; 3 | declare function isHeaderAcceptable(data: Uint8Array): boolean; 4 | -------------------------------------------------------------------------------- /src/core/encodings/jsonxl.ts: -------------------------------------------------------------------------------- 1 | import type { Encoding } from '../utils/load-data.js'; 2 | import { decode, isHeaderAcceptable as test } from './jsonxl-snapshot9.js'; 3 | 4 | export const encoding = /* @__PURE__ */ Object.freeze({ 5 | name: 'jsonxl/snapshot9', 6 | test, 7 | streaming: false, 8 | decode 9 | }) satisfies Encoding as Encoding; 10 | -------------------------------------------------------------------------------- /src/core/encodings/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Encoding } from '../utils/load-data.js'; 2 | 3 | export function validateEncodingConfig(config: any) { 4 | if (!config || typeof config !== 'object') { 5 | return 'value is not an object'; 6 | } 7 | 8 | const { name, test, decode } = config; 9 | 10 | if (typeof name !== 'string') { 11 | return 'missed name'; 12 | } 13 | 14 | if (typeof test !== 'function') { 15 | return 'missed test function'; 16 | } 17 | 18 | if (typeof decode !== 'function') { 19 | return 'missed decode function'; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | export function normalizeEncodingConfig(config: any): Encoding { 26 | const error = validateEncodingConfig(config); 27 | 28 | if (error) { 29 | throw new Error(`Bad encoding config${config?.name ? ` "${config.name}"` : ''}: ${error}`); 30 | } 31 | 32 | const { name, test, streaming, decode } = config as Encoding; 33 | 34 | return Object.freeze(streaming 35 | ? { 36 | name: name || 'unknown', 37 | test, 38 | streaming: true, 39 | decode 40 | } 41 | : { 42 | name: name || 'unknown', 43 | test, 44 | streaming: false, 45 | decode 46 | } 47 | ); 48 | } 49 | 50 | export function normalizeEncodings(encodings?: any[]): Encoding[] { 51 | if (!encodings) { 52 | return []; 53 | } 54 | 55 | if (!Array.isArray(encodings)) { 56 | throw new Error('Encodings must be an array'); 57 | } 58 | 59 | return encodings.map(normalizeEncodingConfig); 60 | } 61 | -------------------------------------------------------------------------------- /src/core/preset.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import type { ViewRenderer } from './view.js'; 4 | import { Dictionary } from './dict.js'; 5 | 6 | export type Preset = { 7 | name: string; 8 | render(el: HTMLElement | DocumentFragment, config: any, data: any, context: any): any; 9 | config: any; 10 | } 11 | 12 | export class PresetRenderer extends Dictionary { 13 | #view: ViewRenderer; 14 | 15 | constructor(view: ViewRenderer) { 16 | super(); 17 | 18 | this.#view = view; 19 | } 20 | 21 | define(name: string, config: any) { 22 | // FIXME: add check that config is serializable object 23 | config = JSON.parse(JSON.stringify(config)); 24 | 25 | return PresetRenderer.define(this, name, Object.freeze({ 26 | name, 27 | render: (el, _, data, context) => this.#view.render(el, config, data, context), 28 | config 29 | })); 30 | } 31 | 32 | render( 33 | container: HTMLElement | DocumentFragment, 34 | name: string, 35 | data?: any, 36 | context?: any 37 | ) { 38 | const preset = this.get(name); 39 | 40 | if (!preset) { 41 | const errorMsg = 'Preset `' + name + '` is not found'; 42 | console.error(errorMsg, name); 43 | 44 | const el = container.appendChild(document.createElement('div')); 45 | el.className = 'discovery-buildin-view-config-error'; 46 | el.textContent = errorMsg; 47 | 48 | return Promise.resolve(); 49 | } 50 | 51 | return preset.render(container, null, data, context); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/core/utils/compare.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from './object-utils.js'; 2 | 3 | export const deepEqual = (a: any, b: any) => equal(a, b, deepEqual); 4 | 5 | export function equal(a: any, b: any, compare = Object.is) { 6 | if (Object.is(a, b)) { 7 | return true; 8 | } 9 | 10 | if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { 11 | return false; 12 | } 13 | 14 | for (const key in a) { 15 | if (hasOwn(a, key)) { 16 | if (!hasOwn(b, key) || !compare(a[key], b[key])) { 17 | return false; 18 | } 19 | } 20 | } 21 | 22 | for (const key in b) { 23 | if (hasOwn(b, key)) { 24 | if (!hasOwn(a, key)) { 25 | return false; 26 | } 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /src/core/utils/copy-text.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import { createElement } from './dom.js'; 4 | 5 | function execCommandFallback(text: string) { 6 | const copyTextBufferEl = createElement('div', { 7 | style: [ 8 | 'position: fixed', 9 | 'overflow: hidden', 10 | 'font-size: 1px', 11 | 'width: 1px', 12 | 'height: 1px', 13 | 'top: 0', 14 | 'left: 0', 15 | 'white-space: pre' 16 | ].join(';') 17 | }); 18 | 19 | document.body.append(copyTextBufferEl); 20 | 21 | try { 22 | const selection = window.getSelection(); 23 | const range = document.createRange(); 24 | 25 | copyTextBufferEl.append(text); 26 | range.selectNodeContents(copyTextBufferEl); 27 | selection?.removeAllRanges(); 28 | selection?.addRange(range); 29 | document.execCommand('copy'); 30 | } finally { 31 | copyTextBufferEl.remove(); 32 | } 33 | } 34 | 35 | export async function copyText(text: string) { 36 | try { 37 | if (navigator.clipboard) { 38 | try { 39 | const permissionStatus = await navigator.permissions.query({ 40 | name: 'clipboard-write' as PermissionName // see https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1245 41 | }); 42 | 43 | if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') { 44 | return navigator.clipboard.writeText(text); 45 | } 46 | } catch (e) { 47 | // Safari does not support the clipboard-write permission and throws a TypeError; 48 | // however, the writeText() method works fine. Attempt to call writeText() before 49 | // resorting to a fallback, believing it will work 50 | if (e instanceof TypeError) { 51 | return navigator.clipboard.writeText(text); 52 | } 53 | } 54 | } 55 | } catch {} 56 | 57 | execCommandFallback(text); 58 | } 59 | -------------------------------------------------------------------------------- /src/core/utils/html.ts: -------------------------------------------------------------------------------- 1 | const delimRegExp = /\.\d+(?:eE[-+]?\d+)?|\B(?=(?:\d{3})+(?:\D|$))/g; 2 | 3 | export function escapeHtml(str: string) { 4 | return str 5 | .replace(/&/g, '&') 6 | .replace(/"/g, '"') 7 | .replace(//g, '>'); 9 | } 10 | 11 | export function numDelimOffsets(value: unknown) { 12 | const strValue = String(value); 13 | const offsets: number[] = []; 14 | let match: RegExpExecArray | null = null; 15 | 16 | while ((match = delimRegExp.exec(strValue)) !== null) { 17 | offsets.push(match.index); 18 | } 19 | 20 | return offsets; 21 | } 22 | 23 | export function numDelim(value: unknown, escape = true) { 24 | const strValue = escape && typeof value !== 'number' 25 | ? escapeHtml(String(value)) 26 | : String(value); 27 | 28 | if (strValue.length > 3) { 29 | return strValue.replace( 30 | delimRegExp, 31 | m => m || '' 32 | ); 33 | } 34 | 35 | return strValue; 36 | } 37 | -------------------------------------------------------------------------------- /src/core/utils/id.ts: -------------------------------------------------------------------------------- 1 | export function randomId() { 2 | return [ 3 | performance.timeOrigin.toString(16), 4 | (10000 * performance.now()).toString(16), 5 | Math.random().toString(16).slice(2) 6 | ].join('-'); 7 | } 8 | -------------------------------------------------------------------------------- /src/core/utils/index-script.ts: -------------------------------------------------------------------------------- 1 | export * as base64 from './base64.js'; 2 | export { equal, deepEqual } from './compare.js'; 3 | export { debounce } from './debounce.js'; 4 | export { randomId } from './id.js'; 5 | export { escapeHtml, numDelim } from './html.js'; 6 | export type { TypedArray } from './is-type.js'; 7 | export { isTypedArray, isArray, isSet, isRegExp } from './is-type.js'; 8 | export { jsonStringifyAsJavaScript, jsonStringifyInfo } from './json.js'; 9 | export type * from './load-data.js'; 10 | export * from './load-data.js'; 11 | export type * from './logger.js'; 12 | export { Logger } from './logger.js'; 13 | export { objectToString, hasOwn } from './object-utils.js'; 14 | export { match, matchAll } from './pattern.js'; 15 | export { safeFilterRx } from './safe-filter-rx.js'; 16 | -------------------------------------------------------------------------------- /src/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './index-script.js'; 2 | 3 | export { applyContainerStyles, rollbackContainerStyles } from './container-styles.js'; 4 | export { copyText } from './copy-text.js'; 5 | export type { CreateElementAttrs } from './dom.js'; 6 | export { createElement, createFragment, createText, isDocumentFragment, passiveCaptureOptions, passiveSupported } from './dom.js'; 7 | export type { InjectStyle, InjectInlineStyle, InjectLinkStyle } from './inject-styles.js'; 8 | export { injectStyles } from './inject-styles.js'; 9 | export { getBoundingRect, getOffsetParent, getOverflowParent, getPageOffset, getViewportRect } from './layout.js'; 10 | export type { LocationSync } from './location-sync.js'; 11 | export { createLocationSync } from './location-sync.js'; 12 | export { getLocalStorageEntry, getLocalStorageValue, getSessionStorageEntry, getSessionStorageValue, PersistentStorageEntry } from './persistent.js'; 13 | export { pointerXY } from './pointer.js'; 14 | export type * from './progressbar.js'; 15 | export { Progressbar } from './progressbar.js'; 16 | export { ContentRect } from './size.js'; 17 | -------------------------------------------------------------------------------- /src/core/utils/is-type.ts: -------------------------------------------------------------------------------- 1 | import { objectToString } from './object-utils.js'; 2 | 3 | export type TypedArray = 4 | | Uint8Array 5 | | Uint8ClampedArray 6 | | Uint16Array 7 | | Uint32Array 8 | | Int8Array 9 | | Int16Array 10 | | Int32Array 11 | | Float32Array 12 | | Float64Array 13 | | BigInt64Array 14 | | BigUint64Array; 15 | 16 | export function isTypedArray(value: unknown): value is TypedArray { 17 | return ArrayBuffer.isView(value) && !(value instanceof DataView); 18 | } 19 | 20 | export function isArray(value: unknown): value is Array | TypedArray { 21 | return Array.isArray(value) || isTypedArray(value); 22 | } 23 | 24 | export function isSet(value: unknown): value is Set { 25 | return objectToString(value) === '[object Set]'; 26 | } 27 | 28 | export function isRegExp(value: unknown): value is RegExp { 29 | return objectToString(value) === '[object RegExp]'; 30 | } 31 | 32 | export function isObject(value: unknown): value is object { 33 | return typeof value === 'object' && value !== null; 34 | } 35 | -------------------------------------------------------------------------------- /src/core/utils/location-sync.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from './logger.js'; 2 | 3 | export type LocationSync = ReturnType; 4 | 5 | export function createLocationSync( 6 | onChange: (newHash: string, oldHash: string) => void, 7 | logger?: Logger 8 | ) { 9 | const location: Location = globalThis.location; 10 | const ignoreHashChange: string[] = []; 11 | const listener = ({ newURL, oldURL }) => { 12 | const newUrlHash = new URL(newURL).hash || '#'; 13 | const oldUrlHash = new URL(oldURL).hash || '#'; 14 | 15 | if (newUrlHash !== ignoreHashChange.shift()) { 16 | logger?.debug('locationSync onChange:', oldUrlHash, '->', newUrlHash); 17 | ignoreHashChange.length = 0; 18 | 19 | onChange(newUrlHash, oldUrlHash); 20 | } 21 | }; 22 | 23 | addEventListener('hashchange', listener); 24 | 25 | return { 26 | set(hash: string, replace: boolean) { 27 | const newHash = hash || '#'; 28 | const currentHash = location.hash || '#'; 29 | 30 | if (currentHash === newHash) { 31 | return; 32 | } 33 | 34 | logger?.debug('locationSync set:', newHash, replace); 35 | ignoreHashChange.push(hash); 36 | 37 | if (replace) { 38 | location.replace(hash); 39 | } else { 40 | location.hash = hash; 41 | } 42 | }, 43 | dispose() { 44 | removeEventListener('hashchange', listener); 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/core/utils/object-utils.ts: -------------------------------------------------------------------------------- 1 | const { toString, hasOwnProperty } = Object.prototype; 2 | 3 | export const objectToString = (value: unknown) => toString.call(value); 4 | export const hasOwn = Object.hasOwn || ((object: object, key: PropertyKey) => hasOwnProperty.call(object, key)); 5 | -------------------------------------------------------------------------------- /src/core/utils/pointer.ts: -------------------------------------------------------------------------------- 1 | import { passiveCaptureOptions } from './dom.js'; 2 | import { Observer } from '../observer.js'; 3 | 4 | export const pointerXY = /* @__PURE__*/ (() => { 5 | const lastPointerXYPublisher = new Observer({ x: 0, y: 0 }, (newCoords, oldCoords) => 6 | newCoords.x !== oldCoords.x || newCoords.y !== oldCoords.y 7 | ); 8 | 9 | document.addEventListener( 10 | 'pointermove', 11 | ({ x, y }) => lastPointerXYPublisher.set({ x, y }), 12 | passiveCaptureOptions 13 | ); 14 | 15 | return lastPointerXYPublisher.readonly; 16 | })(); 17 | -------------------------------------------------------------------------------- /src/core/utils/safe-filter-rx.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | function buildRx(pattern: string, flags?: string) { 4 | try { 5 | return new RegExp('((?:' + pattern + ')+)', flags); 6 | } catch {} 7 | 8 | return new RegExp('((?:' + pattern.replace(/[\[\]\(\)\?\+\*\{\}\\]/g, '\\$&') + ')+)', flags); 9 | } 10 | 11 | export function safeFilterRx(pattern: string, flags = 'i') { 12 | return Object.assign(buildRx(pattern, flags), { 13 | rawSource: pattern 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/core/utils/size.ts: -------------------------------------------------------------------------------- 1 | import { Observer } from '../observer.js'; 2 | 3 | const resizeObserverSupported = typeof ResizeObserver === 'function'; 4 | 5 | export class ContentRect extends Observer { 6 | static supported = resizeObserverSupported; 7 | 8 | private el: HTMLElement | null; 9 | private observer: ResizeObserver | null; 10 | 11 | constructor() { 12 | super(null); 13 | 14 | this.el = null; 15 | this.observer = resizeObserverSupported 16 | ? new ResizeObserver(entries => { 17 | for (const entry of entries) { 18 | this.set(entry.contentRect); 19 | } 20 | }) 21 | : null; 22 | } 23 | 24 | observe(el: HTMLElement | null) { 25 | if (this.observer === null) { 26 | this.el = null; 27 | return; 28 | } 29 | 30 | el = el || null; 31 | 32 | if (this.el !== el) { 33 | if (this.el !== null) { 34 | this.observer.unobserve(this.el); 35 | } 36 | 37 | if (el !== null) { 38 | this.observer.observe(el); 39 | } 40 | 41 | this.el = el; 42 | } 43 | } 44 | 45 | dispose() { 46 | this.el = null; 47 | this.observer = null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/extensions/inspector.css: -------------------------------------------------------------------------------- 1 | @import './inspector/overlay.css'; 2 | @import './inspector/popup.css'; 3 | @import './inspector/popup-sidebar.css'; 4 | @import './inspector/popup-toolbar.css'; 5 | @import './inspector/popup-content.css'; 6 | -------------------------------------------------------------------------------- /src/extensions/inspector/popup-sidebar.css: -------------------------------------------------------------------------------- 1 | .discovery-inspect-details-popup .sidebar { 2 | grid-area: sidebar; 3 | overflow: auto; 4 | overscroll-behavior: contain; 5 | padding: 4px; 6 | background-color: var(--discovery-mate-background); 7 | } 8 | .discovery-inspect-details-popup .sidebar .view-tree-leaf-content { 9 | white-space: nowrap; 10 | padding-right: 12px; 11 | } 12 | .discovery-inspect-details-popup .sidebar .view-root { 13 | display: inline-block; 14 | margin-bottom: 1px; 15 | margin-left: -6px; 16 | border: 4px solid transparent; 17 | border-width: 1px 8px; 18 | background-color: var(--discovery-view-root-highlight-color); 19 | } 20 | .discovery-inspect-details-popup .sidebar .selected { 21 | background-color: rgba(78, 187, 255, .3); 22 | box-shadow: 0 0 0 3px rgba(78, 187, 255, .3); 23 | display: inline; 24 | } 25 | .discovery-inspect-details-popup .sidebar .skipped { 26 | text-decoration: line-through; 27 | font-style: italic; 28 | opacity: .65; 29 | } 30 | 31 | .discovery-inspect-details-popup .sidebar > .view-tree-leaf:only-child { 32 | margin: -4px 0 0 -4px; 33 | grid-template-columns: 6px auto; 34 | background: none; 35 | } 36 | .discovery-inspect-details-popup .sidebar > .view-tree-leaf:only-child > .view-tree-leaf-toggle { 37 | display: none; 38 | } 39 | 40 | .discovery-inspect-details-popup .sidebar .view-badge { 41 | vertical-align: top; 42 | margin-left: 1ex; 43 | margin-right: 0; 44 | border-radius: 0; 45 | font-size: 9px; 46 | } 47 | .discovery-inspect-details-popup .sidebar .view-badge + .view-badge { 48 | margin-left: 1px; 49 | } 50 | -------------------------------------------------------------------------------- /src/extensions/inspector/popup-toolbar.css: -------------------------------------------------------------------------------- 1 | .discovery-inspect-details-popup .toolbar { 2 | grid-area: toolbar; 3 | display: flex; 4 | gap: 1px; 5 | } 6 | 7 | .discovery-inspect-details-popup .stack-view-chain { 8 | flex: 1; 9 | gap: 1px; 10 | background-color: var(--discovery-mate-background); 11 | } 12 | .discovery-inspect-details-popup .stack-view-chain .view-toggle { 13 | border-radius: 0; 14 | margin: 0; 15 | padding: 5px 8px; 16 | line-height: 16px; 17 | } 18 | .discovery-inspect-details-popup .stack-view-chain .skipped { 19 | text-decoration: line-through; 20 | font-style: italic; 21 | opacity: .65; 22 | } 23 | .discovery-inspect-details-popup .stack-view-chain .view-root:not(.checked):not(:hover) { 24 | background-color: var(--discovery-view-root-highlight-color); 25 | } 26 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes { 27 | position: relative; 28 | vertical-align: middle; 29 | display: inline-flex; 30 | gap: 2px; 31 | margin: -5px -2px -2px 4px; 32 | font-size: 7px; 33 | line-height: 12px; 34 | text-align: center; 35 | text-transform: uppercase; 36 | } 37 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes > * { 38 | width: 12px; 39 | height: 12px; 40 | border-radius: 8px; 41 | 42 | overflow: hidden; 43 | box-shadow: 1px 1px 1px rgba(0, 0, 0, .2); 44 | } 45 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes .data { 46 | background: #1f841f80; 47 | } 48 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes .context { 49 | background: #b1366f80; 50 | bottom: 0; 51 | } 52 | 53 | .discovery-inspect-details-popup .toolbar .view-button { 54 | padding: 5px 8px 7px; 55 | font-size: 12px; 56 | line-height: 12px; 57 | border-radius: 2px 1px 2px 2px; 58 | box-shadow: none; 59 | } 60 | -------------------------------------------------------------------------------- /src/extensions/inspector/utils.js: -------------------------------------------------------------------------------- 1 | export function normalizeSource(text) { 2 | text = text 3 | // cut first empty lines 4 | .replace(/^(?:\s*[\n]+)+?([ \t]*)/, '$1') 5 | .trimRight(); 6 | 7 | // fix empty strings 8 | text = text.replace(/\n[ \t]+(?=\n)/g, '\n'); 9 | 10 | // normalize text offset 11 | const lines = text.split(/\n+/); 12 | const startLine = Number(text.match(/^\s/) === null); 13 | let minOffset = 1000; 14 | 15 | for (var i = startLine; i < lines.length; i++) { 16 | const m = lines[i].match(/^\s*/); 17 | 18 | if (m[0].length < minOffset) { 19 | minOffset = m[0].length; 20 | } 21 | 22 | if (minOffset == 0) { 23 | break; 24 | } 25 | } 26 | 27 | if (minOffset > 0) { 28 | text = text.replace(new RegExp('^ {' + minOffset + '}', 'gm'), ''); 29 | } 30 | 31 | return text; 32 | } 33 | -------------------------------------------------------------------------------- /src/extensions/modelfree.ts: -------------------------------------------------------------------------------- 1 | import { ViewModel } from '../main/view-model.js'; 2 | 3 | export default (host: ViewModel) => { 4 | let defaultPageId = ''; 5 | 6 | host.nav.remove('index-page'); 7 | host.nav.remove('discovery-page'); 8 | 9 | host.on('data', () => { 10 | if (host.defaultPageId !== host.discoveryPageId) { 11 | defaultPageId = host.defaultPageId; 12 | host.defaultPageId = host.discoveryPageId; 13 | host.setPageHash(host.pageHash, true); 14 | host.cancelScheduledRender(); 15 | } 16 | }); 17 | host.on('unloadData', () => { 18 | if (defaultPageId !== host.defaultPageId) { 19 | host.defaultPageId = defaultPageId; 20 | host.setPageHash(host.pageHash, true); 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/extensions/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import type { ViewModel } from '../main/view-model.js'; 3 | import type { LocationSync } from '../core/utils/location-sync.js'; 4 | import { createLocationSync } from '../core/utils/location-sync.js'; 5 | 6 | export default function(host: ViewModel) { 7 | function createHostLocationSync() { 8 | return createLocationSync(hash => host.setPageHash(hash), host.logger); 9 | } 10 | 11 | let locationSync: LocationSync | null = createHostLocationSync(); 12 | 13 | // init 14 | host.setPageHash(location.hash); 15 | host.cancelScheduledRender(); 16 | 17 | // register action 18 | host.action.define('permalink', (hash: string) => new URL(hash, String(location)).href); 19 | host.action.define('setPreventLocationUpdate', (prevent: boolean = true) => { 20 | if (prevent) { 21 | locationSync?.dispose(); 22 | locationSync = null; 23 | } else if (locationSync === null) { 24 | locationSync = createHostLocationSync(); 25 | } 26 | }); 27 | 28 | // sync 29 | host.on('pageHashChange', (replace) => locationSync?.set(host.pageHash, replace)); 30 | } 31 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/favicon.png -------------------------------------------------------------------------------- /src/lib-script.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version.js'; 2 | import { Model } from './main/model.js'; 3 | import * as textViews from './text-views/index.js'; 4 | import { encoding as jsonxl } from './core/encodings/jsonxl.js'; 5 | import * as utils from './core/utils/index-script.js'; 6 | 7 | export type * from './main/model.js'; 8 | export { 9 | version, 10 | Model, 11 | textViews, 12 | jsonxl, 13 | utils 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib.css: -------------------------------------------------------------------------------- 1 | @import url('./pages/index.css'); 2 | @import url('./views/index.css'); 3 | @import url('./main/index.css'); 4 | 5 | .discovery-buildin-view-render-error { 6 | display: inline-block; 7 | vertical-align: top; 8 | margin: 1px; 9 | border: 1px solid rgba(212, 0, 0, 0.4); 10 | background-image: linear-gradient(to bottom, rgba(255, 25, 25, 0.1) 19px, transparent 0); 11 | background-clip: padding-box; 12 | color: #c66; 13 | border-radius: 4px; 14 | padding: 4px; 15 | font-size: 10px; 16 | line-height: 1; 17 | } 18 | .discovery-buildin-view-render-error::before { 19 | content: 'ERROR'; 20 | display: inline-block; 21 | margin: -4px 1ex -4px -4px; 22 | border-radius: 3px 0 0 3px; 23 | background: rgb(226, 36, 36, .4); 24 | color: rgba(255, 255, 255, .85); 25 | text-shadow: 1px 1px rgb(0, 0, 0, .2); 26 | padding: 4px; 27 | } 28 | 29 | .discovery-buildin-view-render-error[data-type="config"]::before { 30 | content: 'CONFIG ERROR'; 31 | } 32 | .discovery-buildin-view-render-error[data-type="render"]::before { 33 | content: 'RENDER ERROR'; 34 | } 35 | 36 | .discovery-buildin-view-render-error.expanded::before { 37 | border-bottom-left-radius: 0; 38 | } 39 | .discovery-buildin-view-render-error .toggle-config { 40 | margin-left: 1ex; 41 | cursor: pointer; 42 | opacity: .65; 43 | color: #888; 44 | user-select: none; 45 | } 46 | .discovery-buildin-view-render-error .toggle-config:hover { 47 | opacity: 1; 48 | } 49 | .discovery-buildin-view-render-error .view-struct { 50 | margin: 5px -4px -4px; 51 | border-radius: 0 0 3px 3px; 52 | } 53 | 54 | .discovery-buildin-view-tooltip { 55 | padding: 5px 10px; 56 | min-width: 120px; 57 | border: 0.5px solid #fff5; 58 | border-radius: 3px; 59 | font-size: 12px; 60 | background: rgba(255, 255, 255, 0.75); 61 | -webkit-backdrop-filter: blur(4px); 62 | backdrop-filter: blur(4px); 63 | } 64 | .discovery-root-darkmode .discovery-buildin-view-tooltip { 65 | background: rgba(36, 36, 36, 0.8); 66 | } 67 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version.js'; 2 | import { Model, ViewModel, App } from './main/index.js'; 3 | import * as textViews from './text-views/index.js'; 4 | import * as views from './views/index.js'; 5 | import * as pages from './pages/index.js'; 6 | import inspector from './extensions/inspector.js'; 7 | import upload from './extensions/upload.js'; 8 | import router from './extensions/router.js'; 9 | import embed from './extensions/embed-client.js'; 10 | import { buttons as navButtons } from './nav/index.js'; 11 | import { encoding as jsonxl } from './core/encodings/jsonxl.js'; 12 | import * as utils from './core/utils/index.js'; 13 | 14 | export type * from './main/index.js'; 15 | export type * from './views/editor/editors.js'; 16 | export { 17 | version, 18 | Model, 19 | ViewModel, 20 | ViewModel as Widget, // for backward compatibility 21 | App, 22 | textViews, 23 | views, 24 | pages, 25 | embed, 26 | inspector, 27 | jsonxl, 28 | router, 29 | upload, 30 | navButtons, 31 | utils 32 | }; 33 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/index.css: -------------------------------------------------------------------------------- 1 | @import url('../views/index.css'); 2 | @import url('../pages/index.css'); 3 | @import url('../extensions/inspector.css'); 4 | @import url('./view-model.css'); 5 | @import url('./app.css'); 6 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.js'; 2 | export * from './view-model.js'; 3 | export { App } from './app.js'; 4 | -------------------------------------------------------------------------------- /src/main/view-model.css: -------------------------------------------------------------------------------- 1 | @import './style/base.css'; 2 | @import './style/sidebar.css'; 3 | @import '../nav/index.css'; 4 | -------------------------------------------------------------------------------- /src/nav/img/burger-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/nav/img/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/nav/img/clipboard.png -------------------------------------------------------------------------------- /src/nav/img/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/nav/img/inspect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/default.css: -------------------------------------------------------------------------------- 1 | .page-default h1.no-data-loaded { 2 | margin-top: 0; 3 | } 4 | .page-default h1.no-data-loaded::before { 5 | display: inline-block; 6 | vertical-align: middle; 7 | position: relative; 8 | top: -5px; 9 | margin-right: 10px; 10 | width: 42px; 11 | height: 42px; 12 | content: ''; 13 | background: var(--discovery-app-icon) no-repeat center; 14 | background-size: 42px; 15 | } 16 | 17 | .welcome-block { 18 | margin-top: -10px; 19 | } 20 | .welcome-block .upload-data { 21 | margin-top: 1.5em; 22 | margin-left: -13px; 23 | background-color: #00000008; 24 | border: 1px dashed #a8a8a8; 25 | padding: 12px; 26 | border-radius: 7px; 27 | max-width: 620px; 28 | } 29 | .discovery-root-darkmode .welcome-block .upload-data { 30 | background-color: #00000014; 31 | border-color: #555; 32 | } 33 | .welcome-block .upload-notes { 34 | font-size: 82%; 35 | color: #aaa; 36 | padding: 10px 0 0 3px; 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/default.js: -------------------------------------------------------------------------------- 1 | export default function(host) { 2 | host.page.define('default', { 3 | view: 'switch', 4 | content: [ 5 | { 6 | when: 'no #.datasets', 7 | content: 'preset/welcome-block' 8 | }, 9 | { 10 | content: [ 11 | 'page-header{ content: "h1:#.name" }', 12 | { view: 'struct', expanded: 1 } 13 | ] 14 | } 15 | ] 16 | }); 17 | 18 | // default welcome block 19 | host.preset.define('welcome-block', { 20 | view: 'block', 21 | className: 'welcome-block', 22 | data: '#.model', 23 | content: [ 24 | 'app-header', 25 | 26 | { 27 | view: 'block', 28 | className: 'upload-data', 29 | when: '#.actions.uploadFile', 30 | content: [ 31 | 'preset/upload', 32 | { 33 | view: 'block', 34 | className: 'upload-notes', 35 | content: 'html:name + " is a server-less application that securely opens and analyzes your data directly on your device,
ensuring all processing is done locally without transmitting your data elsewhere."' 36 | } 37 | ] 38 | } 39 | ] 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/discovery.css: -------------------------------------------------------------------------------- 1 | @import url('./discovery/header.css'); 2 | @import url('./discovery/editor-common.css'); 3 | @import url('./discovery/editor-query.css'); 4 | @import url('./discovery/editor-view.css'); 5 | 6 | .page-discovery { 7 | padding-top: 20px !important; 8 | } 9 | .discovery:not([data-dzen]) .page-discovery > .discovery-render-content { 10 | min-height: calc(100vh - 121px); 11 | } 12 | 13 | /******************************** 14 | * Error 15 | ********************************/ 16 | 17 | .page-discovery > .discovery-editor .discovery-error, 18 | .page-discovery > .discovery-render-content > .discovery-error { 19 | display: block; 20 | overflow: hidden; 21 | border-left: 3px solid rgba(255, 0, 0, 0.8); 22 | background: rgba(225, 75, 75, 0.2); 23 | background-clip: padding-box; 24 | padding: 8px 12px; 25 | font-size: 12px; 26 | white-space: pre-wrap; 27 | font-family: var(--discovery-monospace-font-family); 28 | font-size: 11px; 29 | } 30 | .page-discovery > .discovery-editor .discovery-error::before, 31 | .page-discovery > .discovery-render-content > .discovery-error::before { 32 | display: block; 33 | margin-bottom: .5em; 34 | font-size: 16px; 35 | } 36 | .page-discovery > .discovery-editor .query-error::before { 37 | content: 'Query error'; 38 | } 39 | .page-discovery > .discovery-render-content > .render-error::before { 40 | content: 'Render error'; 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/discovery/img/clone-root.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/discovery/img/clone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/formatting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/fullscreen-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/discovery/img/perform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/discovery/img/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/stash-root.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/stash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/subquery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/img/suggestions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/discovery/types.ts: -------------------------------------------------------------------------------- 1 | export type KnownParams = { 2 | dzen: boolean; 3 | noedit: boolean; 4 | title: string; 5 | query: string; 6 | graph: Graph; 7 | view: string | undefined; 8 | viewEditorHidden: boolean; 9 | }; 10 | export type Params = KnownParams & { 11 | [key: string]: unknown; 12 | }; 13 | export type UpdateHostParams = (patch: Partial, replace?: boolean) => void; 14 | export type Graph = { 15 | current: number[]; 16 | children: GraphNode[]; 17 | }; 18 | export type GraphNode = Partial<{ 19 | query: string; 20 | view: string; 21 | children: GraphNode[]; 22 | }>; 23 | export type Computation = { 24 | state: 'successful' | 'failed' | 'awaiting' | 'computing' | 'canceled'; 25 | path: number[]; 26 | query: string; 27 | error: Error | null; 28 | data: unknown; 29 | context: unknown; 30 | computed: unknown; 31 | duration: number; 32 | }; 33 | -------------------------------------------------------------------------------- /src/pages/index.css: -------------------------------------------------------------------------------- 1 | @import url('./default.css'); 2 | @import url('./discovery.css'); 3 | @import url('./views-showcase.css'); 4 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from './default.js'; 2 | export { default as notFound } from './not-found.js'; 3 | export { default as discovery } from './discovery.js'; 4 | export { default as viewsShowcase } from './views-showcase.js'; 5 | -------------------------------------------------------------------------------- /src/pages/not-found.js: -------------------------------------------------------------------------------- 1 | export default function(host) { 2 | host.page.define('not-found', [ 3 | 'alert-warning:"Page \`" + name + "\` not found"' 4 | ]); 5 | } 6 | -------------------------------------------------------------------------------- /src/preloader.css: -------------------------------------------------------------------------------- 1 | @import './views/controls/progress.css'; 2 | 3 | :host { 4 | /* all: initial; */ 5 | position: absolute; 6 | z-index: 1001 !important; 7 | padding: 35px 40px; 8 | width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/text-views/alert.js: -------------------------------------------------------------------------------- 1 | import usage from './alert.usage.js'; 2 | 3 | export default function({ textView }) { 4 | textView.define('alert', { 5 | type: 'block', 6 | usage, 7 | border: { 8 | top: ['╔', '═', '╗'], 9 | left: '║ ', 10 | right: ' ║', 11 | bottom: ['╚', '═', '╝'] 12 | }, 13 | render(node, props, data, context) { 14 | const { content = 'text' } = props; 15 | 16 | return textView.render(node, content, data, context); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/text-views/alert.usage.js: -------------------------------------------------------------------------------- 1 | export default (view) => ({ 2 | demo: `${view}:"hello world\\nsome text..."` 3 | }); 4 | -------------------------------------------------------------------------------- /src/text-views/badge.js: -------------------------------------------------------------------------------- 1 | import usage from './badges.usage.js'; 2 | 3 | export default function({ textView }) { 4 | const commonConfig = { 5 | type: 'inline-block', 6 | usage, 7 | props: `is not array? | { 8 | text: #.props has no 'content' ? is (string or number or boolean) ?: text, 9 | content: undefined, 10 | prefix, 11 | postfix 12 | } | overrideProps()`, 13 | render(node, props, data, context) { 14 | const { text, content, prefix, postfix } = props; 15 | let render; 16 | 17 | if (prefix) { 18 | node.appendText(String(prefix) + ' '); 19 | } 20 | 21 | if (content) { 22 | render = this.render(node, content, data, context); 23 | } else { 24 | node.appendText(String(text)); 25 | } 26 | 27 | if (postfix) { 28 | node.appendText(' ' + String(postfix)); 29 | } 30 | 31 | return render; 32 | } 33 | }; 34 | 35 | textView.define('badge', { 36 | ...commonConfig, 37 | border: { 38 | left: ['⎡', '⎢', '⎣', '['], 39 | right: ['⎤', '⎥', '⎦', ']'] 40 | // left: ['╭ ', '│', '╰', '['], 41 | // right: [' ╮', '│', '╯', ']'] 42 | } 43 | }); 44 | textView.define('pill-badge', { 45 | ...commonConfig, 46 | border: { 47 | left: ['⎧', '⎪', '⎩', '('], 48 | right: ['⎫', '⎪', '⎭', ')'] 49 | } 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /src/text-views/badges.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: { 3 | view, 4 | text: 'hello world' 5 | }, 6 | examples: [ 7 | { 8 | title: 'Variations', 9 | demo: group.map(name => [ 10 | `${name}:"${name}"`, 11 | `${name}:"${name}\\nmulti-line"`, 12 | `${name}:"${name}\\nmulti-line\\nmore..."` 13 | ]).flat() 14 | }, 15 | { 16 | title: 'Complex content', 17 | highlightProps: ['content'], 18 | demo: { 19 | view, 20 | content: ['text:"text"', 'link:"#example"', 'text:"text"'] 21 | } 22 | } 23 | ] 24 | }); 25 | -------------------------------------------------------------------------------- /src/text-views/base-blocks.js: -------------------------------------------------------------------------------- 1 | import usage from './base-blocks.usage.js'; 2 | 3 | export default function({ textView }) { 4 | function render(node, props, data, context) { 5 | const { content = 'text', border } = props; 6 | 7 | if (border) { 8 | node.setBorder(border); 9 | } 10 | 11 | return textView.render(node, content, data, context); 12 | } 13 | 14 | textView.define('inline', { type: 'inline', usage, render }); 15 | textView.define('inline-block', { type: 'inline-block', usage, render }); 16 | textView.define('block', { type: 'block', usage, render }); 17 | textView.define('line', { type: 'line', usage, render }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/text-views/base-blocks.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: `${view}:'just a text'`, 3 | examples: [ 4 | { 5 | title: 'Variations', 6 | demo: group.map(name => [ 7 | `header:"${name}"`, 8 | 'text:"…text…"', 9 | `${name}:"⎡${name}⎦"`, 10 | 'text:"…text…"', 11 | `${name}:"⎡${name}\\nmulti-line⎦"`, 12 | 'text:"…text…"', 13 | `${name}:"⎡${name}\\nmulti-line\\nmore and more…⎦"`, 14 | 'text:"…text…"' 15 | ]).flat() 16 | }, 17 | { 18 | title: 'Variations with border', 19 | demo: group.map(name => [ 20 | `header:"${name}"`, 21 | 'text:"…text…"', 22 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}⎦" }`, 23 | 'text:"…text…"', 24 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}\\nmulti-line⎦" }`, 25 | 'text:"…text…"', 26 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}\\nmulti-line\\nmore and more…⎦" }`, 27 | 'text:"…text…"' 28 | ]).flat() 29 | } 30 | ] 31 | }); 32 | -------------------------------------------------------------------------------- /src/text-views/blockquote.js: -------------------------------------------------------------------------------- 1 | import usage from './blockquote.usage.js'; 2 | 3 | export default function({ textView }) { 4 | textView.define('blockquote', function(node, props, data, context) { 5 | const { content = 'text', kind } = props; 6 | 7 | if (typeof kind === 'string' && /\S/.test(kind)) { 8 | node.appendLine().appendText(`[!${kind.trim().toUpperCase()}]`); 9 | } 10 | 11 | return textView.render(node, content, data, context); 12 | }, { 13 | type: 'block', 14 | usage, 15 | border: { 16 | left: '> ' 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/text-views/blockquote.usage.js: -------------------------------------------------------------------------------- 1 | import usage from '../views/text/blockquote.usage.js'; 2 | export default usage; 3 | -------------------------------------------------------------------------------- /src/text-views/context.js: -------------------------------------------------------------------------------- 1 | import usage from './context.usage.js'; 2 | 3 | export default function({ textView }) { 4 | textView.define('context', function(node, props, data, context) { 5 | const { content = [] } = props; 6 | 7 | return textView.render(node, content, data, context); 8 | }, { usage }); 9 | } 10 | -------------------------------------------------------------------------------- /src/text-views/context.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: ['md:"A non-visual view used to group views, manage their visibility, or explicitly define data and context for nested views. In the example, the `context` view sets up specific data and context, allowing nested views (`content`) to utilize these shared definitions directly:"'], 3 | demo: { 4 | view: 'context', 5 | data: { name: 'World', age: '1000 years' }, 6 | context: '{ ...#, greeting: "Hello" }', 7 | content: [ 8 | 'text:`${#.greeting}, ${name}!`', 9 | 'table' 10 | ] 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/text-views/headers.js: -------------------------------------------------------------------------------- 1 | import usage from './headers.usage.js'; 2 | 3 | export default function({ textView }) { 4 | function renderHeader(level) { 5 | const prefix = '#'.repeat(level) + ' '; 6 | 7 | return async function render(node, config, data, context) { 8 | const { content } = config; 9 | 10 | node.appendText(prefix); 11 | return textView.render(node, content || 'text', data, context); 12 | }; 13 | } 14 | 15 | textView.define('header', renderHeader(1), { type: 'block', usage }); 16 | textView.define('h1', renderHeader(1), { type: 'block', usage }); 17 | textView.define('h2', renderHeader(2), { type: 'block', usage }); 18 | textView.define('h3', renderHeader(3), { type: 'block', usage }); 19 | textView.define('h4', renderHeader(4), { type: 'block', usage }); 20 | textView.define('h5', renderHeader(5), { type: 'block', usage }); 21 | } 22 | -------------------------------------------------------------------------------- /src/text-views/headers.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: `${view}:"Header \\"${view}\\""`, 3 | examples: [ 4 | { 5 | title: 'Variations', 6 | view: group.map(view => `${view}:"Header \\"${view}\\""`) 7 | }, 8 | { 9 | title: 'Complex content', 10 | demo: { 11 | view, 12 | content: [ 13 | 'text:"Text "', 14 | 'link:{ text: "Link" }' 15 | ] 16 | } 17 | }, 18 | { 19 | title: 'Using anchor', 20 | demo: [ 21 | { 22 | view, 23 | anchor: 'foo', 24 | content: 'text:"Explicit value for an anchor"' 25 | }, 26 | { 27 | view, 28 | anchor: true, 29 | content: 'text:"Auto generated anchor based on text content of header"' 30 | } 31 | ] 32 | } 33 | ] 34 | }); 35 | -------------------------------------------------------------------------------- /src/text-views/index.js: -------------------------------------------------------------------------------- 1 | export * as alert from './alert.js'; 2 | export * as badge from './badge.js'; 3 | export * as block from './base-blocks.js'; 4 | export * as blockquote from './blockquote.js'; 5 | export * as context from './context.js'; 6 | export * as headerViews from './headers.js'; 7 | export * as link from './link.js'; 8 | export * as listViews from './lists.js'; 9 | export * as source from './source.js'; 10 | export * as switchView from './switch.js'; 11 | export * as table from './table.js'; 12 | export * as textViews from './text.js'; 13 | -------------------------------------------------------------------------------- /src/text-views/link.js: -------------------------------------------------------------------------------- 1 | import usage from './link.usage.js'; 2 | 3 | const props = `is not array? | { 4 | text: #.props.content is undefined ? is string ?: text, 5 | content: undefined, 6 | href 7 | } | overrideProps() | { 8 | $text; $href; 9 | ..., 10 | text: $text | is not undefined or no $href ?: $href, 11 | href: $href | is not undefined or no $text ?: $text 12 | }`; 13 | 14 | export default function({ textView }) { 15 | textView.define('link', async function(node, props, data, context) { 16 | let { 17 | text, 18 | content, 19 | href 20 | } = props; 21 | 22 | if (href) { 23 | node.appendText('['); 24 | } 25 | 26 | if (content) { 27 | await textView.render(node, content, data, context); 28 | } else { 29 | node.appendText(text); 30 | } 31 | 32 | if (href) { 33 | node.appendText(`](${href})`); 34 | } 35 | }, { 36 | type: 'inline-block', 37 | props, 38 | usage 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/text-views/link.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'link', 4 | text: 'I am a link', 5 | href: '#example' 6 | }, 7 | examples: [ 8 | { 9 | title: 'Infering text ⇿ href', 10 | highlightProps: ['text', 'href', 'data'], 11 | beforeDemo: ['md:"When `text` is omitted but `href` is specified, or vice versa, the opposite component is inferred from the specified one"'], 12 | demo: [ 13 | { view: 'link', text: 'http://example1.com' }, 14 | { view: 'link', href: 'http://example2.com' }, 15 | { view: 'link', data: '"http://example3.com"' } 16 | ] 17 | } 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /src/text-views/lists.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: { 3 | view, 4 | data: ['one', 'two', 'three', 'four'] 5 | }, 6 | examples: [ 7 | { 8 | title: 'Variations', 9 | demoData: ['foo', 'bar', 'baz'], 10 | demo: group.map(view => [ 11 | `header:'view: \\"${view}\\"'`, 12 | view 13 | ]).flat() 14 | }, 15 | { 16 | title: 'Variations long lists', 17 | demoData: Array.from({ length: 100 }, (_, idx) => 'item#' + idx), 18 | demo: group.map(view => [ 19 | `header:'view: \\"${view}\\"'`, 20 | { view: view, limit: 3 } 21 | ]).flat() 22 | }, 23 | { 24 | title: 'Configure item\'s content', 25 | demo: [ 26 | { 27 | view, 28 | data: ['one', 'two', 'three', 'four'], 29 | item: [ 30 | 'text:" "', 31 | { 32 | view: 'link', 33 | data: '{ href: "#" + $ }' 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | }); 41 | -------------------------------------------------------------------------------- /src/text-views/switch.js: -------------------------------------------------------------------------------- 1 | import usage from './switch.usage.js'; 2 | 3 | export default function(host) { 4 | host.textView.define('switch', function(node, config, data, context) { 5 | let { content } = config; 6 | let renderConfig = 'text:""'; 7 | 8 | if (Array.isArray(content)) { 9 | for (const branch of content) { 10 | if (branch && host.queryBool(branch.when || true, data, context)) { 11 | renderConfig = 'data' in branch 12 | ? { 13 | view: 'context', 14 | data: branch.data, 15 | content: branch.content 16 | } 17 | : branch.content; 18 | break; 19 | } 20 | } 21 | } 22 | 23 | return host.textView.render(node, renderConfig, data, context); 24 | }, { usage }); 25 | } 26 | -------------------------------------------------------------------------------- /src/text-views/switch.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'switch', 4 | content: [ 5 | { 6 | when: 'expr', 7 | content: 'text:"Renders when `expr` is truthy"' 8 | }, 9 | { 10 | content: 'text:"Renders when all other `when` conditions are falsy"' 11 | } 12 | ] 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/text-views/table.usage.js: -------------------------------------------------------------------------------- 1 | const defaultDemoData = [ 2 | { name: 'Alice', age: 34, occupation: 'Engineer' }, 3 | { name: 'Bob', age: 42, occupation: 'Doctor' }, 4 | { name: 'Charlie', age: 9, occupation: 'Student' }, 5 | { name: 'David', age: 50, occupation: 'Doctor' }, 6 | { name: 'Eve', age: 15, occupation: 'Engineer' } 7 | ]; 8 | export default { 9 | demoData: defaultDemoData, 10 | demo: { 11 | view: 'table', 12 | limit: 2, 13 | cols: { 14 | age: { footer: 'text:sum(=>age)' }, 15 | occupation: { footer: {} } 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/text-views/text.js: -------------------------------------------------------------------------------- 1 | import usage from './text.usage.js'; 2 | 3 | export default function({ textView }) { 4 | textView.define('text', { 5 | props: "{ text: #.props has no 'text'? } | overrideProps()", 6 | usage, 7 | render(node, props) { 8 | const { text } = props; 9 | node.appendText(String(text)); 10 | } 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/text-views/text.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: 'text:"just a text"' 3 | }; 4 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = '0.0.0-dev'; 2 | -------------------------------------------------------------------------------- /src/views/button.css: -------------------------------------------------------------------------------- 1 | /* For backward capability with discovery-cli */ 2 | @import './controls/button.css'; 3 | -------------------------------------------------------------------------------- /src/views/charts/histogram.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: ['md:"A histogram"'], 3 | demoData: [0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 1, 2, 5, 25, 14, 10, 25, 22, 20, 20], 4 | demo: { 5 | view: 'histogram' 6 | }, 7 | examples: [ 8 | { 9 | title: 'Sizes', 10 | demoData: [0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 1, 2, 5], 11 | demo: { 12 | view: 'table', 13 | context: '{ ...#, values: $ }', 14 | rows: ['xs', 's', 'm', 'l', 'xl'], 15 | cols: [ 16 | { header: 'size' }, 17 | { header: 'histogram', content: ['text:123', 'histogram{ size: $, dataset: #.values }'] } 18 | ] 19 | } 20 | // ['xs', 's', 'm', 'l', 'xl'].map(size => [ 21 | // `header:"${size}"`, 22 | // `histogram{ size: "${size}" }` 23 | // ]).flat() 24 | } 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /src/views/context.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: ['md:"A non-visual view used to group views, manage their visibility, or explicitly define data and context for nested views. In the example, the `context` view sets up specific data and context, allowing nested views (`content`) to utilize these shared definitions directly:"'], 3 | demo: { 4 | view: 'context', 5 | data: { name: 'World', age: '1000 years' }, 6 | context: '{ ...#, greeting: "Hello" }', 7 | content: [ 8 | 'text:`${#.greeting}, ${name}!`', 9 | 'table' 10 | ] 11 | }, 12 | examples: [ 13 | { 14 | title: 'Using with modifiers', 15 | demo: { 16 | view: 'context', 17 | modifiers: [ 18 | 'h2:"Modifiers"', 19 | { 20 | view: 'input', 21 | name: 'inputValue' 22 | }, 23 | { 24 | view: 'select', 25 | name: 'selectValue', 26 | data: ['foo', 'bar', 'baz'] 27 | } 28 | ], 29 | content: [ 30 | 'h2:"Values"', 31 | 'struct{ expanded: 1, data: # }' 32 | ] 33 | } 34 | } 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /src/views/controls/button.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import usage from './button.usage.js'; 4 | import { createElement } from '../../core/utils/dom.js'; 5 | 6 | const props = `is not array? | { 7 | text: #.props has no 'content' ? is (string or number or boolean) ?: text, 8 | content: undefined, 9 | disabled: false, 10 | href, 11 | external, 12 | onClick: undefined 13 | } | overrideProps()`; 14 | 15 | export default function(host) { 16 | function render(el, props, data, context) { 17 | const { 18 | text, 19 | content, 20 | disabled, 21 | href, 22 | external, 23 | onClick 24 | } = props; 25 | 26 | el.classList.add('view-button'); 27 | 28 | if (disabled) { 29 | el.disabled = true; 30 | } else if (typeof onClick === 'function') { 31 | el.addEventListener('click', () => onClick(el, data, context)); 32 | el.classList.add('onclick'); 33 | } else if (typeof href === 'string') { 34 | el.addEventListener('click', () => { 35 | const tmpLinkEl = createElement('a', { 36 | href, 37 | target: external ? '_blank' : '' 38 | }); 39 | 40 | // temporary add element into a document (to host's root) to allow a click interception 41 | host.dom.root.append(tmpLinkEl); 42 | tmpLinkEl.click(); 43 | tmpLinkEl.remove(); 44 | }); 45 | } 46 | 47 | if (content) { 48 | return host.view.render(el, content, data, context); 49 | } else { 50 | el.textContent = text; 51 | } 52 | } 53 | 54 | host.view.define('button', render, { tag: 'button', props, usage }); 55 | host.view.define('button-primary', render, { tag: 'button', props, usage }); 56 | host.view.define('button-danger', render, { tag: 'button', props, usage }); 57 | host.view.define('button-warning', render, { tag: 'button', props, usage }); 58 | } 59 | -------------------------------------------------------------------------------- /src/views/controls/button.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default (view, group) => ({ 4 | demo: { 5 | view, 6 | text: 'Button', 7 | onClick: Function('return () => alert("Hello world!")')() 8 | }, 9 | examples: [ 10 | { 11 | title: 'Variations', 12 | demo: group.map(name => `${name}{ text: "${name}" }`) 13 | }, 14 | { 15 | title: 'Disabled state', 16 | demo: group.map(name => ({ 17 | view: name, 18 | disabled: true, 19 | text: name 20 | })) 21 | }, 22 | { 23 | title: 'Complex content', 24 | demo: { 25 | view: 'button', 26 | content: [ 27 | 'text:"Example "', 28 | 'pill-badge:123' 29 | ] 30 | } 31 | }, 32 | { 33 | title: 'Button as a link', 34 | demo: { 35 | view: 'button', 36 | text: 'Click me', 37 | href: '#url', 38 | external: true 39 | } 40 | }, 41 | { 42 | title: 'Using data as source of options', 43 | beforeDemo: { 44 | view: 'md', 45 | source: [ 46 | 'The following properties are taken from the data when the appropriate options are not specified for a legacy reasons (is subject to remove in the future):', 47 | '- `text`', 48 | '- `href`', 49 | '- `external`' 50 | ].join('\n') 51 | }, 52 | highlightProps: ['data'], 53 | demo: { 54 | view, 55 | data: { 56 | text: 'demo', 57 | href: '#example', 58 | external: true 59 | } 60 | } 61 | } 62 | ] 63 | }); 64 | -------------------------------------------------------------------------------- /src/views/controls/checkbox-list.css: -------------------------------------------------------------------------------- 1 | .view-checkbox-list > .view-checkbox { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/controls/checkbox-list.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './checkbox-list.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('checkbox-list', function(el, config, data, context) { 6 | const { name = 'filter', checkbox, checkboxValue = '$', emptyText, limit, onChange, onInit } = config; 7 | const state = new Set(); 8 | 9 | if (emptyText !== false && emptyText !== '') { 10 | el.setAttribute('emptyText', emptyText || 'Empty list'); 11 | } 12 | 13 | if (!Array.isArray(data) && data) { 14 | data = [data]; 15 | } 16 | 17 | if (Array.isArray(data)) { 18 | return host.view.renderList(el, this.composeConfig({ 19 | view: 'checkbox', 20 | ...checkbox, 21 | onInit: (checked, _, itemData, itemContext) => { 22 | if (checked) { 23 | state.add(host.query(checkboxValue, itemData, itemContext)); 24 | } 25 | }, 26 | onChange: (checked, _, itemData, itemContext) => { 27 | const size = state.size; 28 | const value = host.query(checkboxValue, itemData, itemContext); 29 | 30 | if (checked) { 31 | state.add(value); 32 | } else { 33 | state.delete(value); 34 | } 35 | 36 | if (size !== state.size && typeof onChange === 'function') { 37 | onChange([...state], name); 38 | } 39 | } 40 | }), data, context, 0, host.view.listLimit(limit, 25)).then(() => { 41 | if (typeof onInit === 'function') { 42 | onInit([...state], name); 43 | } 44 | }); 45 | } 46 | }, { usage }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/views/controls/checkbox-list.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'checkbox-list', 4 | checkbox: { 5 | content: 'text' 6 | }, 7 | data: [ 8 | 'one', 9 | 'two', 10 | 'three' 11 | ] 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/views/controls/checkbox.css: -------------------------------------------------------------------------------- 1 | .view-checkbox { 2 | --discovery-checkbox-size: max(1em, 14px); 3 | padding-left: var(--discovery-checkbox-size); 4 | } 5 | .view-checkbox > input { 6 | -webkit-appearance: none; 7 | appearance: none; 8 | content: ''; 9 | font-size: inherit; 10 | display: inline-block; 11 | position: relative; 12 | top: 2px; 13 | box-sizing: border-box; 14 | height: var(--discovery-checkbox-size); 15 | width: var(--discovery-checkbox-size); 16 | margin: 0; 17 | margin-left: calc(-1 * var(--discovery-checkbox-size)); 18 | background-color: rgba(255, 255, 255, .2); 19 | background-repeat: no-repeat; 20 | background-position: center; 21 | background-size: calc(.57 * var(--discovery-checkbox-size)); 22 | border: 1px solid rgba(151, 162, 172, 0.65); 23 | border-radius: 3px; 24 | outline: none; 25 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 26 | } 27 | .view-checkbox > input:checked { 28 | border-color: transparent; 29 | background-color: rgba(0, 121, 232, 0.68); 30 | background-image: url('./checkbox.svg'); 31 | } 32 | .view-checkbox > input:focus-visible { 33 | box-shadow: 0 0 1px 3px rgba(0, 170, 255, .2); 34 | } 35 | .view-checkbox > input:focus-visible:not(:checked) { 36 | border-color: rgba(0, 141, 255, 0.75); 37 | } 38 | .view-checkbox > input:active { 39 | border-color: transparent; 40 | background-color: rgba(25, 139, 236, .6); 41 | } 42 | .view-checkbox > input[readonly], 43 | .view-checkbox > input:disabled { 44 | box-shadow: none; 45 | border-color: rgba(165, 165, 165, 0.1); 46 | background-color: rgba(165, 165, 165, 0.3); 47 | } 48 | 49 | .view-checkbox__label { 50 | margin-left: 5px; 51 | margin-right: 15px; 52 | } 53 | -------------------------------------------------------------------------------- /src/views/controls/checkbox.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createElement } from '../../core/utils/dom.js'; 3 | import usage from './checkbox.usage.js'; 4 | 5 | const props = `#.props | { 6 | name: undefined, 7 | checked: undefined, 8 | readonly: undefined, 9 | content: undefined, 10 | onInit: undefined, 11 | onChange: undefined 12 | } | overrideProps() | { 13 | ..., 14 | checked is truthy, 15 | readonly is truthy 16 | }`; 17 | 18 | export default function(host) { 19 | function renderContent(contentEl, content, data, context, name, inputEl) { 20 | if (contentEl === null) { 21 | return; 22 | } 23 | 24 | const localContext = name ? { ...context, [name]: inputEl.checked } : context; 25 | 26 | contentEl.innerHTML = ''; 27 | return host.view.render(contentEl, content, data, localContext); 28 | } 29 | 30 | host.view.define('checkbox', function(el, config, data, context) { 31 | const { name, checked, readonly, content, onInit, onChange } = config; 32 | const inputEl = el.appendChild(createElement('input')); 33 | const contentEl = content ? el.appendChild(createElement('span', 'view-checkbox__label')) : null; 34 | 35 | inputEl.type = 'checkbox'; 36 | inputEl.checked = checked; 37 | inputEl.readOnly = readonly; 38 | inputEl.addEventListener('click', (e) => { 39 | if (readonly) { 40 | e.preventDefault(); 41 | } 42 | }); 43 | inputEl.addEventListener('change', () => { 44 | if (typeof onChange === 'function') { 45 | onChange(inputEl.checked, name, data, context); 46 | renderContent(contentEl, content, data, context, name, inputEl); 47 | } 48 | }); 49 | 50 | if (typeof onInit === 'function') { 51 | onInit(inputEl.checked, name, data, context); 52 | } 53 | 54 | return renderContent(contentEl, content, data, context, name, inputEl); 55 | }, { 56 | tag: 'label', 57 | props, 58 | usage 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/views/controls/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/controls/checkbox.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default { 4 | demo: { 5 | view: 'checkbox', 6 | content: 'text:"checkbox caption"' 7 | }, 8 | examples: [ 9 | { 10 | title: 'Checked state', 11 | beforeDemo: 'Checked state is set up with `checked` property. Its value can be a query', 12 | demo: [ 13 | { 14 | view: 'checkbox', 15 | checked: true, 16 | content: 'text:"should be checked"' 17 | }, 18 | { 19 | view: 'checkbox', 20 | checked: '=1 > 5', 21 | content: 'text:"shouldn\'t be checked"' 22 | }, 23 | { 24 | view: 'checkbox', 25 | checked: '=1 < 5', 26 | content: 'text:"should be checked"' 27 | } 28 | ] 29 | }, 30 | { 31 | title: 'Readonly checkbox', 32 | demo: { 33 | view: 'checkbox', 34 | readonly: true, 35 | content: 'text:"checkbox caption"' 36 | } 37 | }, 38 | { 39 | title: 'On change', 40 | demo: { 41 | view: 'checkbox', 42 | // eslint-disable-next-line no-unused-vars 43 | onChange: Function('return (value, name, data, context) => alert(`Changed to ${value}!`)')(), 44 | content: 'text:"click me!"' 45 | } 46 | } 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /src/views/controls/content-filter.css: -------------------------------------------------------------------------------- 1 | .view-content-filter > .view-input { 2 | line-height: 1; 3 | margin-bottom: 1em; 4 | } 5 | 6 | .view-content-filter > .view-input input { 7 | background-image: url('./content-filter.svg'); 8 | background-repeat: no-repeat; 9 | background-size: 32px 16px; 10 | background-position: right center; 11 | padding-right: 30px; 12 | } 13 | 14 | .view-content-filter > .content { 15 | overflow: auto; 16 | flex: 1; 17 | } 18 | .view-content-filter > .content .view-list::before { 19 | padding: 5px 10px; 20 | display: block; 21 | } 22 | -------------------------------------------------------------------------------- /src/views/controls/content-filter.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './content-filter.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('content-filter', function(el, config, data, context) { 6 | const { name = 'filter', type = 'regexp', placeholder, content, onInit, onChange, debounce } = config; 7 | 8 | return host.view.render(el, { 9 | view: 'context', 10 | modifiers: { 11 | view: 'input', 12 | name, 13 | type, 14 | placeholder: placeholder || 'Filter', 15 | debounce 16 | }, 17 | content: { 18 | view: 'block', 19 | className: 'content', 20 | content, 21 | onInit, 22 | onChange 23 | } 24 | }, data, context); 25 | }, { 26 | usage 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/views/controls/content-filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/controls/content-filter.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'content-filter', 4 | data: ['foo', 'bar', 'baz'], 5 | content: { 6 | view: 'list', 7 | data: '.[$ ~= #.filter]' 8 | } 9 | }, 10 | examples: [ 11 | { 12 | title: 'Using with text-match', 13 | demo: { 14 | view: 'content-filter', 15 | data: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }], 16 | name: 'customName', 17 | content: { 18 | view: 'list', 19 | data: '.[name ~= #.customName]', 20 | item: 'text-match:{ text: name, match: #.customName }' 21 | } 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/views/controls/dropdown.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | export default { 3 | demo: { 4 | view: 'context', 5 | modifiers: { 6 | view: 'dropdown', 7 | name: 'demo', 8 | value: { foo: 'two', bar: 'hello' }, 9 | resetValue: { foo: 'one', bar: '' }, 10 | caption: 'text:`${#.demo.foo} / ${#.demo.bar}`', 11 | content: [ 12 | { view: 'select', name: 'foo', data: ['one', 'two', 'three', 'four'] }, 13 | { view: 'input', name: 'bar' } 14 | ] 15 | }, 16 | content: [ 17 | { view: 'block', content: 'text:"Modified context (see values in \\"demo\\" section):"' }, 18 | { view: 'struct', expanded: 1, data: '#' } 19 | ] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/views/controls/input.css: -------------------------------------------------------------------------------- 1 | .view-input input { 2 | position: relative; 3 | width: 100%; 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 8px 12px; 7 | font-family: inherit; 8 | font-size: inherit; 9 | line-height: 1.2; 10 | border: 1px solid rgba(153, 153, 153, 0.5); 11 | border-radius: 3px; 12 | background-color: rgba(255, 255, 255, .05); 13 | color: var(--discovery-color); 14 | outline: 0; 15 | } 16 | .view-input input:hover { 17 | border-color: rgba(153, 153, 153, 0.75); 18 | } 19 | .view-input input:focus { 20 | border-color: rgba(0, 170, 255, .65); 21 | box-shadow: 0 0 1px 3px rgba(0, 170, 255, .2), inset 0 1px 1px rgba(142, 142, 142, .2); 22 | z-index: 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/views/controls/input.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | const onChange = Function('return (value) => alert(`Changed to ${value}!`)')(); 3 | 4 | export default { 5 | examples: [ 6 | { 7 | title: 'Input with value', 8 | demo: { 9 | view: 'input', 10 | value: '"value"' 11 | } 12 | }, 13 | { 14 | title: 'Input with placeholder', 15 | demo: { 16 | view: 'input', 17 | placeholder: 'placeholder' 18 | } 19 | }, 20 | { 21 | title: 'Input type number with min and max', 22 | demo: { 23 | view: 'input', 24 | htmlType: 'number', 25 | htmlMin: 10, 26 | htmlMax: 20 27 | } 28 | }, 29 | { 30 | title: 'Input with onChange', 31 | demo: { 32 | view: 'input', 33 | onChange: onChange 34 | } 35 | }, 36 | { 37 | title: 'Input with onChange debounced', 38 | demo: { 39 | view: 'input', 40 | onChange: onChange, 41 | debounce: 300 42 | } 43 | } 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/controls/menu-item.css: -------------------------------------------------------------------------------- 1 | .view-menu-item { 2 | display: block; 3 | padding: 4px 12px; 4 | color: inherit; 5 | text-decoration: none; 6 | cursor: pointer; 7 | } 8 | .view-menu-item:empty::before { 9 | content: '\200B'; 10 | float: left; 11 | } 12 | .view-menu-item:hover, 13 | .view-menu-item.discovery-view-popup-active { 14 | background: rgba(131, 131, 131, 0.25); 15 | } 16 | .view-menu-item.selected { 17 | background: rgba(178, 221, 248, 0.3); 18 | cursor: default; 19 | } 20 | .view-menu-item.disabled { 21 | opacity: .65; 22 | background: none; 23 | pointer-events: none; 24 | } 25 | .view-menu-item:not(.onclick):not([href]) { 26 | pointer-events: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/views/controls/menu-item.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './menu-item.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('menu-item', function(el, config, data, context) { 6 | const { content, onClick } = config; 7 | const { text, selected = false, disabled = false, href, external } = data || {}; 8 | 9 | if (disabled) { 10 | el.classList.add('disabled'); 11 | } else if (typeof onClick === 'function') { 12 | el.addEventListener('click', () => onClick(data, context)); 13 | el.classList.add('onclick'); 14 | } else if (href) { 15 | el.href = href; 16 | el.target = external ? '_blank' : ''; 17 | } 18 | 19 | if (selected) { 20 | el.classList.add('selected'); 21 | } 22 | 23 | if (content) { 24 | return host.view.render(el, content, data, context); 25 | } else { 26 | el.textContent = typeof data === 'string' ? data : text || 'Untitled item'; 27 | } 28 | }, { 29 | tag: 'a', 30 | usage 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/views/controls/menu-item.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'menu', 4 | data: [ 5 | { text: 'one', href: '#' }, 6 | { text: 'two', href: '#' }, 7 | { text: 'three', href: '#' } 8 | ] 9 | }, 10 | examples: [ 11 | { 12 | title: 'Preselected item', 13 | demo: { 14 | view: 'menu', 15 | data: [ 16 | { text: 'one', href: '#' }, 17 | { text: 'two', href: '#', selected: true}, 18 | { text: 'three', href: '#' } 19 | ] 20 | } 21 | }, 22 | { 23 | title: 'Disabled item', 24 | demo: { 25 | view: 'menu', 26 | data: [ 27 | { text: 'one', href: '#' }, 28 | { text: 'two', href: '#', disabled: true}, 29 | { text: 'three', href: '#' } 30 | ] 31 | } 32 | }, 33 | { 34 | title: 'External links', 35 | demo: { 36 | view: 'menu', 37 | data: [ 38 | { text: 'one', external: true, href: 'https://github.com/discoveryjs/discovery' }, 39 | { text: 'two', external: true, href: 'https://github.com/discoveryjs/discovery' }, 40 | { text: 'three', external: true, href: 'https://github.com/discoveryjs/discovery' } 41 | ] 42 | } 43 | } 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/controls/menu.css: -------------------------------------------------------------------------------- 1 | .view-menu:empty::before { 2 | content: attr(emptyText); 3 | display: block; 4 | padding: 4px 12px; 5 | color: #888; 6 | } 7 | 8 | .view-menu > .more-buttons { 9 | margin: 2px 12px 8px; 10 | } 11 | -------------------------------------------------------------------------------- /src/views/controls/menu.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './menu.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('menu', function(el, config, data, context) { 6 | const { name = 'filter', item, itemConfig, limit, emptyText, onClick, onInit, onChange } = config; 7 | 8 | if (emptyText !== false && emptyText !== '') { 9 | el.setAttribute('emptyText', emptyText || 'No items'); 10 | } 11 | 12 | if (Array.isArray(data)) { 13 | const composedItemConfig = this.composeConfig({ 14 | view: 'menu-item', 15 | content: item, 16 | onClick: typeof onClick === 'function' 17 | ? onClick 18 | : typeof onChange === 'function' 19 | ? (data) => onChange(data, name) 20 | : undefined 21 | }, itemConfig); 22 | 23 | return host.view.renderList( 24 | el, 25 | composedItemConfig, 26 | data, 27 | context, 28 | 0, 29 | host.view.listLimit(limit, 25) 30 | ).then(() => { 31 | if (typeof onInit === 'function') { 32 | onInit(data.find(item => item.selected), name); 33 | } 34 | }); 35 | } 36 | }, { usage }); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/controls/menu.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | const onChange = Function('return (value) => alert(`Changed to ${value.text}!`)')(); 3 | 4 | export default { 5 | demo: { 6 | view: 'menu', 7 | data: [ 8 | { text: 'one', href: '#' }, 9 | { text: 'two', href: '#' }, 10 | { text: 'three', href: '#' } 11 | ] 12 | }, 13 | examples: [ 14 | { 15 | title: 'With limit', 16 | demo: { 17 | view: 'menu', 18 | data: [ 19 | { text: 'one', href: '#' }, 20 | { text: 'two', href: '#' }, 21 | { text: 'three', href: '#' } 22 | ], 23 | limit: 2 24 | } 25 | }, 26 | { 27 | title: 'With custom item', 28 | demo: { 29 | view: 'menu', 30 | data: [ 31 | { text: 'one', href: '#' }, 32 | { text: 'two', href: '#' }, 33 | { text: 'three', href: '#' } 34 | ], 35 | item: 'h1:text' 36 | } 37 | }, 38 | { 39 | title: 'On chage handler', 40 | demo: { 41 | view: 'menu', 42 | onChange: onChange, 43 | data: [ 44 | { text: 'one', href: '#' }, 45 | { text: 'two', href: '#' }, 46 | { text: 'three', href: '#' } 47 | ] 48 | } 49 | } 50 | ] 51 | }; 52 | -------------------------------------------------------------------------------- /src/views/controls/nav-button.css: -------------------------------------------------------------------------------- 1 | .view-nav-button { 2 | vertical-align: top; 3 | display: inline-block; 4 | min-height: 1.6em; 5 | padding: 5px 15px 6px; 6 | color: #444; 7 | background-color: rgba(155, 155, 155, 0.15); 8 | font-size: 12px; 9 | text-decoration: none; 10 | outline: none; 11 | cursor: pointer; 12 | } 13 | .view-nav-button:hover, 14 | .view-nav-button:focus, 15 | .view-nav-button.discovery-view-popup-active { 16 | color: black; 17 | background-color: rgba(151, 151, 151, 0.3); 18 | } 19 | .view-nav-button + .view-nav-button { 20 | margin-left: 1px 21 | } 22 | .view-nav-button.disabled, 23 | .view-nav-button:not(.onclick):not([href]) { 24 | pointer-events: none; 25 | } 26 | 27 | .discovery-root-darkmode .view-nav-button { 28 | color: inherit; 29 | } 30 | -------------------------------------------------------------------------------- /src/views/controls/nav-button.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './nav-button.usage.js'; 3 | 4 | const props = `is not array? | { 5 | name: undefined, 6 | text: #.props has no 'content' ? is (string or number or boolean) ?: text, 7 | content: undefined, 8 | disabled: false, 9 | href, 10 | external, 11 | onClick: undefined 12 | } | overrideProps()`; 13 | 14 | export default function(host) { 15 | host.view.define('nav-button', function(el, props, data, context) { 16 | const { 17 | name, 18 | text, 19 | content, 20 | disabled, 21 | href, 22 | external, 23 | onClick 24 | } = props; 25 | 26 | if (name) { 27 | el.dataset.name = name; 28 | } 29 | 30 | if (disabled) { 31 | el.classList.add('disabled'); 32 | } else if (typeof onClick === 'function') { 33 | el.addEventListener('click', (event) => onClick(el, data, context, event)); 34 | el.classList.add('onclick'); 35 | } else if (href) { 36 | el.href = href; 37 | el.target = external ? '_blank' : ''; 38 | } 39 | 40 | if (content) { 41 | return host.view.render(el, content, data, context); 42 | } else { 43 | el.textContent = text; 44 | } 45 | }, { 46 | tag: 'a', 47 | props, 48 | usage 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/views/controls/nav-button.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | const onClick = () => alert('changed!'); 3 | 4 | export default { 5 | demo: { 6 | view: 'nav-button', 7 | data: { text: 'I am nav button' } 8 | }, 9 | examples: [ 10 | { 11 | title: 'With href', 12 | demo: { 13 | view: 'nav-button', 14 | data: { text: 'I am nav button', href: '#' } 15 | } 16 | }, 17 | { 18 | title: 'External link', 19 | demo: { 20 | view: 'nav-button', 21 | data: { text: 'I am nav button', external: true, href: 'https://github.com/discoveryjs/discovery' } 22 | } 23 | }, 24 | { 25 | title: 'On click handler', 26 | demo: { 27 | view: 'nav-button', 28 | data: { text: 'I am nav button'}, 29 | onClick 30 | } 31 | } 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /src/views/controls/progress.css: -------------------------------------------------------------------------------- 1 | .view-progress { 2 | max-width: 300px; 3 | z-index: 1; 4 | pointer-events: none; 5 | } 6 | 7 | .view-progress.skip-fast-track { 8 | transition: opacity .25s var(--appearance-delay, 500ms); 9 | will-change: opacity; 10 | contain: paint; 11 | 12 | @starting-style { 13 | opacity: 0; 14 | } 15 | } 16 | .view-progress.skip-fast-track.done:not(.error) { 17 | display: none; 18 | } 19 | 20 | .view-progress > .progress { 21 | content: ''; 22 | display: block; 23 | position: relative; 24 | overflow: hidden; 25 | margin-top: 4px; 26 | box-sizing: border-box; 27 | height: 3px; 28 | background: rgba(198, 198, 198, 0.3); 29 | border-radius: 2px; 30 | } 31 | .view-progress > .progress::before { 32 | content: ''; 33 | display: block; 34 | height: 100%; 35 | width: 100%; 36 | position: absolute; 37 | left: 0; 38 | top: 0; 39 | transform: scaleX(var(--progress, 0)); 40 | transform-origin: left; 41 | /* transition: transform .2s; */ /* since Chrome (tested on 85) freezes transition during js loop */ 42 | background-color: var(--color, #1f7ec5); 43 | } 44 | 45 | .view-progress > .content.main-secondary { 46 | display: flex; 47 | white-space: nowrap; 48 | gap: 1ex; 49 | } 50 | .view-progress > .content > .secondary { 51 | flex: 1; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | color: #888; 55 | } 56 | -------------------------------------------------------------------------------- /src/views/controls/progress.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createElement } from '../../core/utils/dom.js'; 3 | import usage from './progress.usage.js'; 4 | 5 | export default function(host) { 6 | host.view.define('progress', function(el, config, data, context) { 7 | const { content, progress, color } = config; 8 | 9 | el.append(createElement('div', { 10 | class: 'progress', 11 | style: `--progress: ${Math.max(0, Math.min(1, Number(progress)))};--color: ${color || 'unset'};` 12 | })); 13 | 14 | if (content) { 15 | const contentEl = createElement('div', 'content'); 16 | 17 | el.prepend(contentEl); 18 | 19 | return host.view.render(contentEl, content, data, context); 20 | } 21 | }, { usage }); 22 | } 23 | -------------------------------------------------------------------------------- /src/views/controls/progress.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'progress', 4 | progress: .5, 5 | content: 'text:"Loading..."' 6 | }, 7 | examples: [ 8 | { 9 | title: 'With no label (content)', 10 | demo: { 11 | view: 'progress', 12 | progress: .25 13 | } 14 | }, 15 | { 16 | title: 'Custom color', 17 | demo: { 18 | view: 'progress', 19 | progress: .85, 20 | color: '#bdab77', 21 | content: 'text:"Yellow progress"' 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/views/controls/select-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/controls/select.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | const onChange = Function('return (value) => alert(`Changed to ${value}!`)')(); 3 | 4 | export default { 5 | demo: { 6 | view: 'select', 7 | data: ['one', 'two', 'three', 'four'] 8 | }, 9 | examples: [ 10 | { 11 | title: 'Select with value', 12 | demo: { 13 | view: 'select', 14 | value: '"three"', 15 | data: ['one', 'two', 'three', 'four'] 16 | } 17 | }, 18 | { 19 | title: 'Select with reset option', 20 | demo: { 21 | view: 'select', 22 | resetItem: true, 23 | value: '"three"', 24 | data: ['one', 'two', 'three', 'four'] 25 | } 26 | }, 27 | { 28 | title: 'Select with placeholder', 29 | demo: { 30 | view: 'select', 31 | placeholder: 'placeholder', 32 | data: ['one', 'two', 'three', 'four'] 33 | } 34 | }, 35 | { 36 | title: 'Select with onChange', 37 | demo: { 38 | view: 'select', 39 | onChange: onChange, 40 | data: ['one', 'two', 'three', 'four'] 41 | } 42 | }, 43 | { 44 | title: 'Select with custom options', 45 | demo: { 46 | view: 'select', 47 | item: 'h1:text', 48 | data: ['one', 'two', 'three', 'four'] 49 | } 50 | } 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /src/views/controls/tab.css: -------------------------------------------------------------------------------- 1 | .view-tab { 2 | display: inline-block; 3 | position: relative; 4 | padding: 3px 11px; 5 | border-bottom: var(--discovery-view-tabs-border); 6 | font-size: 13px; 7 | cursor: pointer; 8 | white-space: nowrap; 9 | text-transform: capitalize; 10 | } 11 | .view-tab.active { 12 | z-index: 10; 13 | border-bottom: none; 14 | padding-bottom: 4px; 15 | cursor: default; 16 | } 17 | .view-tab.active::before { 18 | content: ''; 19 | position: absolute; 20 | z-index: -1; 21 | top: 0; 22 | left: 0; 23 | right: 0; 24 | bottom: 0; 25 | border: var(--discovery-view-tabs-border); 26 | border-bottom: none; 27 | border-radius: 5px 5px 0 0; 28 | pointer-events: none; 29 | } 30 | .view-tab.disabled { 31 | color: #aaa; 32 | } 33 | .view-tab.disabled, 34 | .view-tab:not(.onclick) { 35 | pointer-events: none; 36 | } 37 | -------------------------------------------------------------------------------- /src/views/controls/tab.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './tab.usage.js'; 3 | 4 | const props = `is not array? | { 5 | value, 6 | text: #.props has no 'content' ? is (string or number or boolean) ?, 7 | content: undefined, 8 | active: undefined, 9 | disabled: undefined, 10 | onClick: undefined 11 | } | overrideProps() | { 12 | ..., 13 | text: text is not undefined ? text : value, 14 | active is truthy, 15 | disabled is truthy 16 | }`; 17 | 18 | export default function(host) { 19 | host.view.define('tab', function(el, props, data, context) { 20 | const { 21 | value, 22 | text, 23 | content, 24 | active, 25 | disabled, 26 | onClick 27 | } = props; 28 | 29 | if (disabled) { 30 | el.classList.add('disabled'); 31 | } else if (typeof onClick === 'function') { 32 | el.addEventListener('click', () => onClick(value)); 33 | el.classList.add('onclick'); 34 | } 35 | 36 | if (active) { 37 | el.classList.add('active'); 38 | } 39 | 40 | if (content) { 41 | return host.view.render(el, content, data, context); 42 | } else { 43 | el.textContent = String(text); 44 | } 45 | }, { usage, props }); 46 | } 47 | -------------------------------------------------------------------------------- /src/views/controls/tabs.css: -------------------------------------------------------------------------------- 1 | .view-tabs-buttons { 2 | --discovery-view-tabs-border: 1px solid rgba(170, 170, 170, 0.4); 3 | margin-top: 2px; 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: flex-end; 7 | } 8 | 9 | .view-tabs-buttons::before, 10 | .view-tabs-buttons::after { 11 | content: ''; 12 | width: 6px; 13 | border-bottom: var(--discovery-view-tabs-border); 14 | } 15 | .view-tabs-buttons::after { 16 | flex: 1; 17 | } 18 | 19 | .view-tabs-buttons-before, 20 | .view-tabs-buttons-after { 21 | padding: 0 6px 3px; 22 | padding-bottom: 3px; 23 | border-bottom: var(--discovery-view-tabs-border); 24 | color: #888; 25 | font-size: 13px; 26 | } 27 | 28 | .view-tabs-content { 29 | overflow: auto; 30 | } 31 | -------------------------------------------------------------------------------- /src/views/controls/toggle-group.css: -------------------------------------------------------------------------------- 1 | .view-toggle-group { 2 | display: inline-flex; 3 | align-items: baseline; 4 | flex-wrap: wrap; 5 | } 6 | .view-toggle-group-before { 7 | padding-right: 1ex; 8 | } 9 | .view-toggle-group-after { 10 | padding-left: 1ex; 11 | order: 1000; 12 | } 13 | 14 | .view-toggle-group .view-toggle + .view-toggle { 15 | margin-left: 1px; 16 | border-top-left-radius: 0; 17 | border-bottom-left-radius: 0; 18 | } 19 | .view-toggle-group .view-toggle:not(:last-child) { 20 | border-top-right-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | -------------------------------------------------------------------------------- /src/views/controls/toggle-group.usage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default { 4 | demo: { 5 | view: 'context', 6 | modifiers: { 7 | view: 'toggle-group', 8 | name: 'toggleValue', 9 | data: [ 10 | { value: 'one', text: 'One' }, 11 | { value: 'two', text: 'Two' }, 12 | { value: 'three', text: 'Three' } 13 | ] 14 | }, 15 | content: { 16 | view: 'switch', 17 | content: [ 18 | { 19 | when: '#.toggleValue="one"', 20 | content: 'text:"One"' 21 | }, 22 | { 23 | when: '#.toggleValue="two"', 24 | content: 'text:"Two"' 25 | }, 26 | { 27 | when: '#.toggleValue="three"', 28 | content: 'text:"Three"' 29 | } 30 | ] 31 | } 32 | }, 33 | examples: [ 34 | { 35 | title: 'With before and after content', 36 | demo: { 37 | view: 'toggle-group', 38 | data: [ 39 | { value: 'one', text: 'One' }, 40 | { value: 'two', text: 'Two' }, 41 | { value: 'three', text: 'Three' } 42 | ], 43 | beforeToggles: 'text:""', 44 | afterToggles: 'text:""' 45 | } 46 | }, 47 | { 48 | title: 'On change handler', 49 | demo: { 50 | view: 'toggle-group', 51 | name: 'example', 52 | onChange: Function('return (value) => alert("changed to " + value)')(), 53 | data: [ 54 | { value: 'one', text: 'One' }, 55 | { value: 'two', text: 'Two' }, 56 | { value: 'three', text: 'Three' } 57 | ] 58 | } 59 | } 60 | ] 61 | }; 62 | -------------------------------------------------------------------------------- /src/views/controls/toggle.css: -------------------------------------------------------------------------------- 1 | .view-toggle { 2 | display: inline-block; 3 | position: relative; 4 | padding: 3px 12px; 5 | background: rgba(200, 200, 200, 0.2); 6 | border-radius: 4px; 7 | font-size: 13px; 8 | cursor: pointer; 9 | white-space: nowrap; 10 | } 11 | .view-toggle:hover { 12 | background-color: rgba(78, 187, 255, .2); 13 | } 14 | .view-toggle.checked { 15 | background-color: rgba(78, 187, 255, .3); 16 | cursor: default; 17 | } 18 | .view-toggle.disabled { 19 | color: #aaa; 20 | } 21 | .view-toggle.disabled, 22 | .view-toggle:not(.onclick) { 23 | pointer-events: none; 24 | } 25 | -------------------------------------------------------------------------------- /src/views/controls/toggle.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './toggle-group.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('toggle', function(el, config, data, context) { 6 | const { 7 | content, 8 | disabled = false, 9 | onToggle, 10 | value, 11 | text = String(value).replace(/^./, m => m.toUpperCase()) 12 | } = config; 13 | let { 14 | checked = false 15 | } = config; 16 | 17 | if (disabled) { 18 | el.classList.add('disabled'); 19 | } else if (typeof onToggle === 'function') { 20 | el.addEventListener('click', () => { 21 | checked = !checked; 22 | onToggle(checked, value); 23 | }); 24 | el.classList.add('onclick'); 25 | } 26 | 27 | if (checked) { 28 | el.classList.add('checked'); 29 | } 30 | 31 | if (content) { 32 | return host.view.render(el, content, data, context); 33 | } else { 34 | el.textContent = text; 35 | } 36 | }, { usage }); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/editor/editor-mode-view.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | 3 | export default function(config, options) { 4 | const isDiscoveryViewDefined = typeof options.isDiscoveryViewDefined === 'function' 5 | ? options.isDiscoveryViewDefined 6 | : () => {}; 7 | const jsMode = CodeMirror.getMode(config, { 8 | name: 'javascript', 9 | json: true 10 | }); 11 | 12 | return { 13 | ...jsMode, 14 | indent(state, textAfter) { 15 | return state.indented + config.indentUnit * ( 16 | (state.lastType === '{' && textAfter.trim()[0] !== '}') || 17 | (state.lastType === '(' && textAfter.trim()[0] !== ')') || 18 | (state.lastType === '[' && textAfter.trim()[0] !== ']') 19 | ); 20 | }, 21 | token: function(stream, state) { 22 | if (state.suspendTokens) { 23 | const { pos, token } = state.suspendTokens.shift(); 24 | 25 | stream.pos = pos; 26 | if (state.suspendTokens.length === 0) { 27 | state.suspendTokens = null; 28 | } 29 | 30 | return token; 31 | } 32 | 33 | const start = stream.pos; 34 | const token = jsMode.token(stream, state); 35 | 36 | if (token === 'string') { 37 | const end = stream.pos; 38 | const [, viewName] = stream.string 39 | .slice(start + 1, end - 1) 40 | .match(/^(.+?)([:{]|$)/) || []; 41 | 42 | if (isDiscoveryViewDefined(viewName)) { 43 | stream.pos = start + 1; 44 | state.suspendTokens = [ 45 | { pos: start + 1 + viewName.length, token: 'string discovery-view-name' }, 46 | { pos: end, token } 47 | ]; 48 | } 49 | } 50 | 51 | return token; 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/views/layout/block.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './block.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('block', function(el, config, data, context) { 6 | const { content = [], onInit, onChange } = config; 7 | const blockContent = typeof onInit !== 'function' && typeof onChange !== 'function' 8 | ? content // left as is since nothing to mix in 9 | : this.composeConfig(content, { 10 | onInit, 11 | onChange 12 | }); 13 | 14 | return host.view.render(el, blockContent, data, context); 15 | }, { usage }); 16 | } 17 | -------------------------------------------------------------------------------- /src/views/layout/block.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: ['md:"A block has no its own look. It\'s using for wrapping some content with a `className` (btw `className` is a common property for any view when appropriate)"'], 3 | demo: { 4 | view: 'block', 5 | className: 'foo', 6 | content: [ 7 | 'text:"Content inside block"' 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/views/layout/column.css: -------------------------------------------------------------------------------- 1 | .view-column { 2 | padding-right: 40px; 3 | min-width: 150px; 4 | } 5 | .view-column:last-child { 6 | padding-right: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/views/layout/column.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './columns.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('column', function(el, config, data, context) { 6 | const { content = [] } = config; 7 | 8 | return host.view.render(el, content, data, context); 9 | }, { usage }); 10 | } 11 | -------------------------------------------------------------------------------- /src/views/layout/columns.css: -------------------------------------------------------------------------------- 1 | .view-columns { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .view-columns:empty::before { 7 | content: attr(emptyText); 8 | color: #888; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/layout/columns.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './columns.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('columns', function render(el, config, data, context) { 6 | const { column, columnConfig, emptyText, limit } = config; 7 | 8 | if (emptyText !== false && emptyText !== '') { 9 | el.setAttribute('emptyText', emptyText || 'Empty'); 10 | } 11 | 12 | if (!Array.isArray(data) && data) { 13 | data = [data]; 14 | } 15 | 16 | if (Array.isArray(data)) { 17 | return host.view.renderList(el, this.composeConfig({ 18 | view: 'column', 19 | content: column 20 | }, columnConfig), data, context, 0, host.view.listLimit(limit, 25)); 21 | } 22 | }, { usage }); 23 | } 24 | -------------------------------------------------------------------------------- /src/views/layout/columns.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'columns', 4 | data: ['one', 'two', 'three', 'four'], 5 | column: 'text' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/views/layout/expand.css: -------------------------------------------------------------------------------- 1 | .view-list > .view-list-item > .view-expand { 2 | margin-bottom: 1px; 3 | } 4 | .view-expand + .view-expand { 5 | margin-top: 1px; 6 | } 7 | .view-list > .view-list-item:not(:last-child) > .view-expand.expanded { 8 | margin-bottom: 4px; 9 | } 10 | .view-expand.expanded + .view-expand { 11 | margin-top: 4px; 12 | } 13 | .view-expand > .header { 14 | --discovery-view-expand-trigger-size: 22px; 15 | 16 | display: flex; 17 | background-color: rgba(192, 192, 192, 0.175); 18 | background-color: color-mix(in srgb, var(--discovery-background-color), rgb(192, 192, 192) 17.5%); 19 | font-size: 12px; 20 | cursor: pointer; 21 | margin-left: calc(var(--discovery-view-expand-trigger-size) + 1px); 22 | } 23 | .view-expand.trigger-outside > .header { 24 | margin-left: 0; 25 | } 26 | .view-expand > .header:hover { 27 | background-color: rgba(165, 165, 165, 0.3); 28 | background-color: color-mix(in srgb, var(--discovery-background-color), rgb(165, 165, 165) 30%); 29 | } 30 | .view-expand > .header > .header-content { 31 | flex: 1; 32 | padding: 2px 8px; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | pointer-events: none; 36 | } 37 | .view-expand > .header > .trigger { 38 | order: -1; 39 | width: var(--discovery-view-expand-trigger-size, 20px); 40 | margin-left: calc(-1 * var(--discovery-view-expand-trigger-size) - 1px); 41 | text-align: center; 42 | background-color: inherit; 43 | background-image: linear-gradient(0deg, rgba(150, 150, 150, .15), rgba(150, 150, 150, .15)); 44 | } 45 | .view-expand > .header > .trigger::before { 46 | background: url(./expand.svg) no-repeat center; 47 | background-size: 12px; 48 | transition: transform .15s ease-in; 49 | width: 12px; 50 | height: 100%; 51 | display: inline-block; 52 | vertical-align: middle; 53 | transform: rotate(-90deg); 54 | content: ''; 55 | } 56 | .view-expand.expanded > .header > .trigger::before { 57 | transform: rotate(0deg); 58 | } 59 | -------------------------------------------------------------------------------- /src/views/layout/expand.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createElement } from '../../core/utils/dom.js'; 3 | import usage from './expand.usage.js'; 4 | 5 | const props = `is not array? | { 6 | header: undefined, 7 | content: undefined, 8 | expanded: false, 9 | onToggle: undefined 10 | } | overrideProps()`; 11 | 12 | export default function(host) { 13 | host.view.define('expand', function(el, config, data, context) { 14 | async function renderState() { 15 | el.classList.toggle('expanded', expanded); 16 | 17 | if (expanded) { 18 | contentEl = createElement('div', 'content'); 19 | 20 | await host.view.render(contentEl, content, data, context) 21 | .then(() => el.appendChild(contentEl)); 22 | } else if (contentEl !== null) { 23 | contentEl.remove(); 24 | contentEl = null; 25 | } 26 | } 27 | 28 | let { expanded, header, content, onToggle } = config; 29 | const headerEl = el.appendChild(createElement('div', 'header')); 30 | const headerContentEl = headerEl.appendChild(createElement('div', 'header-content')); 31 | let contentEl = null; 32 | 33 | headerEl.appendChild(createElement('div', 'trigger')); 34 | headerEl.addEventListener('click', () => { 35 | expanded = !expanded; 36 | 37 | const finish = renderState(); 38 | 39 | if (typeof onToggle === 'function') { 40 | onToggle(expanded, { 41 | el, 42 | finish, 43 | data, 44 | context 45 | }); 46 | } 47 | }); 48 | 49 | return Promise.all([ 50 | host.view.render(headerContentEl, header || 'text:"\u00A0"', data, context), 51 | renderState() 52 | ]); 53 | }, { usage, props }); 54 | } 55 | -------------------------------------------------------------------------------- /src/views/layout/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/layout/expand.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'expand', 4 | header: 'text:"Expand me!"', 5 | content: 'text:"Content"' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/views/layout/hstack.css: -------------------------------------------------------------------------------- 1 | .view-hstack { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: 20px 40px; 5 | } 6 | .view-hstack > * { 7 | min-width: 240px; 8 | } 9 | -------------------------------------------------------------------------------- /src/views/layout/hstack.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './hstack.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('hstack', function(el, config, data, context) { 6 | const { content = [] } = config; 7 | 8 | return host.view.render(el, content, data, context); 9 | }, { usage }); 10 | } 11 | -------------------------------------------------------------------------------- /src/views/layout/hstack.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'hstack', 4 | content: [ 5 | 'button:{text:"First button"}', 6 | 'button-primary:{text:"Second button"}' 7 | ] 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/layout/list-item.css: -------------------------------------------------------------------------------- 1 | .view-inline-list > .view-list-item, 2 | .view-comma-list > .view-list-item { 3 | display: inline-block; 4 | list-style: none; 5 | } 6 | 7 | .view-comma-list > .view-list-item > * { 8 | margin-right: 0; 9 | } 10 | 11 | .view-comma-list > .view-list-item::after { 12 | content: ', '; 13 | white-space: pre; 14 | } 15 | 16 | .view-comma-list > .view-list-item:last-child::after { 17 | content: ''; 18 | } 19 | -------------------------------------------------------------------------------- /src/views/layout/list-item.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default function(host) { 4 | host.view.define('list-item', function(el, config, data, context) { 5 | const { content = 'text' } = config; 6 | 7 | return host.view.render(el, content, data, context); 8 | }, { 9 | tag: 'li' 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/views/layout/lists.css: -------------------------------------------------------------------------------- 1 | .view-list { 2 | display: block; 3 | list-style: none; 4 | } 5 | 6 | .view-ul, 7 | .view-ol { 8 | margin: 0; 9 | padding-left: 0; 10 | } 11 | 12 | .view-ul:not(:empty), 13 | .view-ol:not(:empty) { 14 | padding-left: 20px; 15 | } 16 | 17 | :is(.view-list, .view-ul, .view-ol) + :is(.view-list, .view-ul, .view-ol), 18 | .view-list-item > :is(.view-list, .view-ul, .view-ol) { 19 | margin-top: 0; 20 | margin-bottom: 2px; 21 | } 22 | 23 | .view-inline-list, 24 | .view-comma-list { 25 | display: inline-block; 26 | } 27 | 28 | .view-list:empty::before, 29 | .view-inline-list:empty::before, 30 | .view-ul:empty::before, 31 | .view-ol:empty::before { 32 | content: attr(emptyText); 33 | color: #888; 34 | } 35 | -------------------------------------------------------------------------------- /src/views/layout/lists.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './lists.usage.js'; 3 | 4 | export default function(host) { 5 | function render(el, config, data, context) { 6 | const { item, itemConfig, limit, emptyText } = config; 7 | 8 | if (emptyText !== false && emptyText !== '') { 9 | el.setAttribute('emptyText', emptyText || 'Empty list'); 10 | } 11 | 12 | if (!Array.isArray(data) && data) { 13 | data = [data]; 14 | } 15 | 16 | if (Array.isArray(data)) { 17 | return host.view.renderList(el, this.composeConfig({ 18 | view: 'list-item', 19 | content: item 20 | }, itemConfig), data, context, 0, host.view.listLimit(limit, 25)); 21 | } 22 | } 23 | 24 | host.view.define('list', render, { usage }); 25 | host.view.define('inline-list', render, { usage }); 26 | host.view.define('comma-list', render, { usage }); 27 | host.view.define('ol', render, { tag: 'ol', usage }); 28 | host.view.define('ul', render, { tag: 'ul', usage }); 29 | } 30 | -------------------------------------------------------------------------------- /src/views/layout/lists.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: { 3 | view, 4 | data: ['one', 'two', 'three', 'four'] 5 | }, 6 | examples: [ 7 | { 8 | title: 'Variations', 9 | demo: { 10 | view: 'context', 11 | data: ['foo', 'bar', 'baz'], 12 | content: group.map(view => [ 13 | `header{ content: 'md:${JSON.stringify('`view: \\"' + view + '\\"`')}' }`, 14 | view 15 | ]) 16 | } 17 | }, 18 | { 19 | title: 'Configure item\'s content', 20 | demo: [ 21 | { 22 | view, 23 | data: ['one', 'two', 'three', 'four'], 24 | item: [ 25 | 'text:" "', 26 | { 27 | view: 'link', 28 | data: '{ href: "#" + $ }' 29 | } 30 | ] 31 | } 32 | ] 33 | }, 34 | { 35 | title: 'Configure item\'s config', 36 | demo: { 37 | view, 38 | data: ['one', 'two', 'three', 'four'], 39 | itemConfig: { 40 | className: 'special' 41 | }, 42 | item: { 43 | view: 'text', 44 | data: '"prefix-" + $' 45 | } 46 | } 47 | } 48 | ] 49 | }); 50 | -------------------------------------------------------------------------------- /src/views/layout/page-header.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createElement } from '../../core/utils/dom.js'; 3 | import usage from './page-header.usage.js'; 4 | 5 | export default function(host) { 6 | host.view.define('page-header', function render(el, config, data, context) { 7 | const { prelude, content, onInit, onChange } = config; 8 | const preludeEl = el.appendChild(createElement('div', 'view-page-header__prelude')); 9 | const contentEl = el.appendChild(createElement('div', 'view-page-header__content')); 10 | const mixinHandlers = (config) => 11 | typeof onInit !== 'function' && typeof onChange !== 'function' 12 | ? config // left as is since nothing to mix in 13 | : this.composeConfig(config, { 14 | onInit, 15 | onChange 16 | }); 17 | 18 | return Promise.all([ 19 | host.view.render(preludeEl, mixinHandlers(prelude || []), data, context), 20 | host.view.render(contentEl, mixinHandlers(content || 'text'), data, context) 21 | ]); 22 | }, { usage }); 23 | } 24 | -------------------------------------------------------------------------------- /src/views/layout/page-header.usage.js: -------------------------------------------------------------------------------- 1 | export default (view) => ({ 2 | beforeDemo: [ 3 | 'md:"A special view to be used as the first view in the body of the page. This view stays in place as the page scrolls (although it may move slightly to the top of the page at the start of the scroll), so that the most relevant information and important action elements can remain accessible despite page scrolling."' 4 | ], 5 | demoFixed: 100, 6 | demo: `${view}:"That's a \\"${view}\\""`, 7 | examples: [ 8 | { 9 | title: 'Prelude', 10 | demoFixed: 150, 11 | highlightProps: ['prelude'], 12 | demo: { 13 | view, 14 | prelude: [ 15 | 'badge:{ text: "demo" }', 16 | 'badge:{ text: "demo", prefix: "prelude", postfix: "postfix" }' 17 | ], 18 | content: 'h1:"Header"' 19 | } 20 | } 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /src/views/layout/popup.css: -------------------------------------------------------------------------------- 1 | .discovery-view-popup { 2 | position: fixed; 3 | min-width: 200px; 4 | box-sizing: border-box; 5 | z-index: 300; 6 | overflow: hidden; 7 | overflow-y: auto; 8 | min-height: 20px; 9 | border: 1px solid rgba(176, 176, 176, 0.65); 10 | box-shadow: 3px 3px 18px rgba(0, 0, 0, .2); 11 | background-color: var(--discovery-background-color); 12 | transition-property: background-color; 13 | transition-duration: .25s; 14 | transition-timing-function: ease-in; 15 | } 16 | .discovery-view-popup.inspect { 17 | z-index: 2002; 18 | } 19 | 20 | .discovery-view-popup.show-on-hover:not([data-pin-mode="popup-hover"]):not(.pinned) { 21 | pointer-events: none; 22 | } 23 | 24 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"]::before, 25 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"]::after { 26 | content: "Click to pin this popup"; 27 | display: block; 28 | visibility: hidden; 29 | left: 0; 30 | right: 0; 31 | z-index: 100; 32 | padding: 2px 8px; 33 | background-color: var(--discovery-background-color); 34 | background-image: linear-gradient(to bottom, rgba(0, 0, 0, .05), rgba(0, 0, 0, .05)); 35 | color: #888; 36 | font-size: 10px; 37 | text-align: center; 38 | } 39 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"][data-v-to="top"]::before { 40 | position: absolute; 41 | visibility: visible; 42 | bottom: 0; 43 | } 44 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"][data-v-to="bottom"]::after { 45 | position: absolute; 46 | visibility: visible; 47 | top: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/views/layout/section.css: -------------------------------------------------------------------------------- 1 | .view-section { 2 | margin-bottom: 30px; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/layout/section.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './section.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('section', function(el, config, data, context) { 6 | const { header, content = [], onInit, onChange } = config; 7 | const mixinHandlersIfNeeded = (config) => 8 | typeof onInit !== 'function' && typeof onChange !== 'function' 9 | ? config // left as is since nothing to mix in 10 | : this.composeConfig(config, { 11 | onInit, 12 | onChange 13 | }); 14 | 15 | return host.view.render(el, [ 16 | { view: 'header', content: mixinHandlersIfNeeded(header) }, 17 | mixinHandlersIfNeeded(content) 18 | ], data, context); 19 | }, { usage }); 20 | } 21 | -------------------------------------------------------------------------------- /src/views/layout/section.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'section', 4 | header: 'text:"I am section"', 5 | content: [ 6 | 'text:"content"' 7 | ] 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/layout/toc-section.css: -------------------------------------------------------------------------------- 1 | .view-toc-section > .header { 2 | top: 0px; 3 | position: sticky; 4 | z-index: 1; 5 | padding: 9px 12px; 6 | line-height: 1; 7 | font-size: 14px; 8 | border: 1px solid rgba(85, 85, 85, 0.2); 9 | border-width: 1px 0; 10 | background-color: rgba(250, 250, 250, .92); 11 | text-transform: capitalize; 12 | margin: 0; 13 | margin-bottom: -1px; 14 | transition: background-color .25s ease-in; 15 | } 16 | .discovery-root-darkmode .view-toc-section > .header { 17 | background-color: rgba(50, 50, 50, .92); 18 | border-color: rgba(0, 0, 0, .25); 19 | } 20 | @supports (backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px)) { 21 | .view-toc-section > .header { 22 | background-color: rgba(250, 250, 250, .8); 23 | -webkit-backdrop-filter: blur(5px); 24 | backdrop-filter: blur(5px); 25 | } 26 | .discovery-root-darkmode .view-toc-section > .header { 27 | background-color: rgba(50, 50, 50, .8); 28 | border-color: rgba(0, 0, 0, .25); 29 | } 30 | } 31 | 32 | .view-toc-section > .header .view-badge, 33 | .view-toc-section > .header .view-pill-badge { 34 | text-transform: none; 35 | font-weight: normal; 36 | margin-left: 4px; 37 | margin-right: 0; 38 | } 39 | .view-toc-section > .content { 40 | padding: 8px 0 8px 12px; 41 | white-space: nowrap; 42 | } 43 | -------------------------------------------------------------------------------- /src/views/layout/toc-section.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default function(host) { 4 | host.view.define('toc-section', function(el, config, data, context) { 5 | const { header, content } = config; 6 | 7 | return host.view.render(el, [ 8 | { 9 | view: 'block', 10 | className: 'header', 11 | content: header 12 | }, 13 | { 14 | view: 'block', 15 | className: 'content', 16 | content 17 | } 18 | ], data, context); 19 | }, { 20 | tag: 'section' 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/views/signature/const.js: -------------------------------------------------------------------------------- 1 | export const colors = [ 2 | 'rgba(83,211,75,0.75)', 3 | 'rgba(241,235,44,0.75)', 4 | 'rgba(244,152,99,0.75)', 5 | 'rgba(148,99,244,0.75)', 6 | 'rgba(44,132,241,0.75)', 7 | 'rgba(233,117,117,0.75)', 8 | 'rgba(85,187,155,0.75)', 9 | 'rgba(151,147,99,0.75)', 10 | 'rgba(216,107,196,0.75)', 11 | 'rgba(108,204,227,0.75)', 12 | 'rgba(164,164,164,0.75)' 13 | ]; 14 | 15 | export const typeOrder = [ 16 | 'null', 17 | 'undefined', 18 | 'string', 19 | 'number', 20 | 'bigint', 21 | 'boolean', 22 | 'symbol', 23 | 'function', 24 | 'array', 25 | 'set', 26 | 'object' 27 | ]; 28 | -------------------------------------------------------------------------------- /src/views/signature/signature.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: ['md:"The `signature` view is used to output a type definition for current data using TypeScript like style:"'], 3 | demo: { 4 | view: 'signature', 5 | expanded: true 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/views/signature/style/action-button.css: -------------------------------------------------------------------------------- 1 | .view-signature [data-action] { 2 | display: inline-block; 3 | vertical-align: middle; 4 | margin-left: calc(2em / 9); /* 2px */ 5 | padding: min(calc(1em / 3), 5px) min(calc(3px + 1em / 3), 8px); 6 | margin-top: max(1px - calc(1em / 3), -4px); 7 | color: rgba(170, 170, 170, 0.4); 8 | background-color: rgba(195, 195, 195, 0.05); 9 | border: .85px solid rgba(141, 141, 141, 0.15); 10 | border-radius: 3px; 11 | line-height: 1; 12 | font-size: .75em; /* 9px */ 13 | cursor: pointer; 14 | } 15 | .discovery-root-darkmode .view-signature [data-action] { 16 | --discovery-view-signature-hover-color: #aaa; 17 | --discovery-view-signature-toggle-color: #72b372; 18 | } 19 | .view-signature [data-action][data-enabled=true], 20 | .view-signature [data-action][data-enabled=true]:hover { 21 | color: var(--discovery-view-signature-toggle-color, #333); 22 | background: rgba(151, 223, 151, 0.25); 23 | border-color: rgba(137, 177, 137, 0.4); 24 | } 25 | .view-signature [data-action="collapse"]::before { 26 | content: "–"; 27 | } 28 | .view-signature [data-action="dict-mode"]::before { 29 | content: "dict"; 30 | } 31 | .view-signature [data-action="sort-keys"]::before { 32 | content: "keys ↓"; 33 | } 34 | .view-signature [data-action]:hover { 35 | color: var(--discovery-view-signature-hover-color, #333); 36 | background: rgba(187, 187, 187, 0.25); 37 | border-color: rgba(137, 137, 137, 0.4); 38 | } 39 | -------------------------------------------------------------------------------- /src/views/signature/style/img/group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/struct/popup-signature.js: -------------------------------------------------------------------------------- 1 | function findRootData(context) { 2 | while (context.parent !== null) { 3 | context = context.parent; 4 | } 5 | 6 | return context.host['']; 7 | } 8 | 9 | export function createSignaturePopup(host, elementData, elementContext, elementOptions, buildPathForElement) { 10 | return new host.view.Popup({ 11 | className: 'view-struct-signature-popup', 12 | hoverPin: 'popup-hover', 13 | hoverTriggers: '.view-struct .show-signature', 14 | showDelay: 50, 15 | render(popupEl, triggerEl) { 16 | const el = triggerEl.parentNode; 17 | const data = elementData.get(el); 18 | const context = elementContext.get(el); 19 | const options = elementOptions.get(el) || {}; 20 | 21 | popupEl.classList.add('computing'); 22 | setTimeout(() => { 23 | host.view.render(popupEl, { 24 | view: 'signature', 25 | expanded: 2, 26 | rootData: findRootData(context), 27 | path: buildPathForElement(el) 28 | }, data, options.context).then(() => popupEl.classList.remove('computing')); 29 | }, 16); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/views/switch.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './switch.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('switch', function(el, config, data, context) { 6 | let { content } = config; 7 | let renderConfig = 'alert-warning:"No case choosen"'; 8 | 9 | if (Array.isArray(content)) { 10 | for (let i = 0; i < content.length; i++) { 11 | const branch = content[i]; 12 | 13 | if (branch && host.queryBool(branch.when || true, data, context)) { 14 | renderConfig = 'data' in branch 15 | ? { 16 | view: 'context', 17 | data: branch.data, 18 | content: branch.content 19 | } 20 | : branch.content; 21 | break; 22 | } 23 | } 24 | } 25 | 26 | return host.view.render(el, renderConfig, data, context); 27 | }, { 28 | tag: false, 29 | usage 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/views/switch.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: [ 3 | 'md:"A non-visual view that conditionally renders a single matching section from the provided content. It sequentially evaluates each item\'s `when` expression and renders the content of the first truthy condition. If an item has no `when` property, its condition defaults to truthy, enabling unconditional rendering, often used as a fallback or default content."' 4 | ], 5 | demo: { 6 | view: 'switch', 7 | content: [ 8 | { 9 | when: 'expr', 10 | content: 'text:"Renders when `expr` is truthy"' 11 | }, 12 | { 13 | content: 'text:"Renders when all other `when` conditions are falsy"' 14 | } 15 | ] 16 | }, 17 | examples: [ 18 | { 19 | title: 'Using with context and a modifier', 20 | demo: { 21 | view: 'context', 22 | modifiers: { 23 | view: 'tabs', 24 | tabs: ['foo', 'bar', 'baz'], 25 | name: 'section' 26 | }, 27 | content: { 28 | view: 'switch', 29 | content: [ 30 | { when: '#.section="foo"', content: 'text:"Content for `foo`"' }, 31 | { when: '#.section="bar"', content: 'text:"Content for `bar`"' }, 32 | { content: 'text:"No content is found for `" + #.section + "`"' } 33 | ] 34 | } 35 | } 36 | } 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /src/views/table/table-cell-details-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/table/table-cell.js: -------------------------------------------------------------------------------- 1 | import { applyAlign, createClickHandler, defaultCellRender } from './table-cell-utils.js'; 2 | 3 | export default function(host) { 4 | host.view.define('table-cell', function(el, config, data, context) { 5 | let { content, contentWhen = true, details, detailsWhen = true, colSpan, align } = config; 6 | const isDataObject = 7 | !content && 8 | data !== null && 9 | (Array.isArray(data) ? data.length > 0 : typeof data === 'object') && 10 | data instanceof RegExp === false; 11 | 12 | if (typeof colSpan === 'number' && colSpan > 1) { 13 | el.colSpan = colSpan; 14 | } 15 | 16 | if (!host.queryBool(contentWhen, data, context)) { 17 | return; 18 | } 19 | 20 | applyAlign(el, align); 21 | 22 | if ((details || (details === undefined && isDataObject)) && host.queryBool(detailsWhen, data, context)) { 23 | el.classList.add('details'); 24 | el.addEventListener('click', createClickHandler(host, el, details, data, context)); 25 | } 26 | 27 | if (content) { 28 | return host.view.render(el, content, data, context); 29 | } 30 | 31 | defaultCellRender(el, data, isDataObject); 32 | }, { 33 | tag: 'td' 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/views/table/table-footer-cell.css: -------------------------------------------------------------------------------- 1 | .view-table-footer-cell { 2 | padding: 3 | var(--table-cell-padding-top) 4 | max(calc(var(--table-cell-padding-right) - 1px), 0px) 5 | var(--table-cell-padding-bottom) 6 | var(--table-cell-padding-left); 7 | border: solid var(--discovery-background-color); 8 | border-top-color: transparent; 9 | border-width: 1px 1px 1px 0; 10 | color: #888; 11 | background-color: color-mix(in srgb, var(--discovery-background-color), #8d8d8d 8%); 12 | background-clip: padding-box; 13 | } 14 | .view-table-footer-cell:last-child { 15 | border-right: 0; 16 | } 17 | -------------------------------------------------------------------------------- /src/views/table/table-footer-cell.js: -------------------------------------------------------------------------------- 1 | import { applyAlign, createClickHandler, defaultCellRender } from './table-cell-utils.js'; 2 | 3 | export default function(host) { 4 | host.view.define('table-footer-cell', function(el, config, data, context) { 5 | let { content, contentWhen = true, details, detailsWhen = true, colSpan, align } = config; 6 | const isDataObject = 7 | !content && 8 | data !== null && 9 | (Array.isArray(data) ? data.length > 0 : typeof data === 'object') && 10 | data instanceof RegExp === false; 11 | 12 | el.classList.add('view-table-cell'); 13 | 14 | if (typeof colSpan === 'number' && colSpan > 1) { 15 | el.colSpan = colSpan; 16 | } 17 | 18 | if (!host.queryBool(contentWhen, data, context)) { 19 | return; 20 | } 21 | 22 | applyAlign(el, align); 23 | 24 | if ((details || (details === undefined && isDataObject)) && host.queryBool(detailsWhen, data, context)) { 25 | el.classList.add('details'); 26 | el.addEventListener('click', createClickHandler(host, el, details, data, context)); 27 | } 28 | 29 | if (content) { 30 | return host.view.render(el, content, data, context); 31 | } 32 | 33 | defaultCellRender(el, data, isDataObject); 34 | }, { 35 | tag: 'td' 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/table/table-footer.css: -------------------------------------------------------------------------------- 1 | .view-table-footer.no-cells-with-content { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/table/table-header-cell.css: -------------------------------------------------------------------------------- 1 | .view-table-header-cell { 2 | padding: 3 | var(--table-cell-padding-top) 4 | var(--table-cell-padding-right) 5 | var(--table-cell-padding-bottom) 6 | var(--table-cell-padding-left); 7 | border: solid var(--discovery-background-color); 8 | border-width: 1px 1px 1px 0; 9 | font-weight: normal; 10 | text-align: left; 11 | background: #8d8d8d26 no-repeat right 1px center; 12 | background-color: color-mix(in srgb, var(--discovery-background-color), #8d8d8d 15%); 13 | background-size: 16px; 14 | background-clip: padding-box; 15 | } 16 | .view-table-header-cell:last-child { 17 | border-right: none; 18 | } 19 | .view-table-header-cell.sortable { 20 | padding-right: 18px; 21 | background-image: url('./table-sortable.svg'); 22 | cursor: pointer; 23 | } 24 | .view-table-header-cell.sortable:not(.asc):not(.desc) { 25 | background-size: 14px; 26 | background-position: right 2px center; 27 | } 28 | .view-table-header-cell.sortable.asc { 29 | background-image: url('./table-sort-asc.svg'); 30 | } 31 | .view-table-header-cell.sortable.desc { 32 | background-image: url('./table-sort-desc.svg'); 33 | } 34 | .view-table-header-cell.sortable:hover { 35 | background-color: rgba(141, 141, 141, 0.3); 36 | background-color: color-mix(in srgb, var(--discovery-background-color), rgba(141, 141, 141) 30%); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/table/table-header-cell.js: -------------------------------------------------------------------------------- 1 | export default function(host) { 2 | host.view.define('table-header-cell', function(el, config, data, context) { 3 | let { content, text, initSorting, nextSorting } = config; 4 | 5 | if (typeof nextSorting === 'function') { 6 | el.classList.add('sortable'); 7 | el.addEventListener('click', () => nextSorting( 8 | el.classList.contains('asc') ? 'asc' 9 | : el.classList.contains('desc') ? 'desc' 10 | : 'none' 11 | )); 12 | } 13 | 14 | if (initSorting) { 15 | el.classList.add(initSorting > 0 ? 'asc' : 'desc'); 16 | } 17 | 18 | if (content) { 19 | return host.view.render(el, content, data, context); 20 | } else { 21 | el.textContent = text ?? ''; 22 | } 23 | }, { 24 | tag: 'th' 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/views/table/table-header.css: -------------------------------------------------------------------------------- 1 | .view-table-header { 2 | position: sticky; 3 | top: 0px; 4 | z-index: 1; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/table/table-row.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/views/table/table-row.css -------------------------------------------------------------------------------- /src/views/table/table-row.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default function(host) { 4 | host.view.define('table-row', function(el, config, data, context) { 5 | const { cols } = config; 6 | 7 | if (Array.isArray(cols)) { 8 | return Promise.all(cols.map((col, index) => 9 | host.view.render(el, col, data, { ...context, colIndex: index }) 10 | )); 11 | } 12 | }, { tag: 'tr' }); 13 | } 14 | -------------------------------------------------------------------------------- /src/views/table/table-sort-asc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/table/table-sort-desc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/table/table-sortable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/table/table.css: -------------------------------------------------------------------------------- 1 | @import url('./table-cell.css'); 2 | @import url('./table-header.css'); 3 | @import url('./table-header-cell.css'); 4 | @import url('./table-footer.css'); 5 | @import url('./table-footer-cell.css'); 6 | @import url('./table-row.css'); 7 | 8 | .view-table { 9 | --table-cell-padding-top: 2px; 10 | --table-cell-padding-right: 8px; 11 | --table-cell-padding-bottom: 2px; 12 | --table-cell-padding-left: 8px; 13 | 14 | font-size: 12px; 15 | line-height: 19px; 16 | /* border-collapse: collapse; */ 17 | border-spacing: 0; 18 | } 19 | 20 | .view-table > tbody > .view-table-row:not(:last-child) > td, 21 | .view-table > tbody:not(:last-child) > .view-table-row:last-child > td, 22 | .view-table > .view-table-more-buttons:not(:last-child) > tr > td:not(:empty) { 23 | border-bottom: 1px solid rgba(170, 170, 170, 0.2); 24 | } 25 | .view-table > tbody.view-table-more-buttons > tr > td > .more-buttons { 26 | margin-bottom: 2px; 27 | } 28 | .view-table > tbody.view-table-more-buttons > tr > td:empty { 29 | padding: 0; 30 | } 31 | 32 | .view-table-cell-details-row > .view-cell-details-content > .view-table:first-child { 33 | margin-top: -1px !important; 34 | } 35 | -------------------------------------------------------------------------------- /src/views/text-render.css: -------------------------------------------------------------------------------- 1 | .view-text-render { 2 | font-family: var(--discovery-monospace-font-family); 3 | font-size: var(--discovery-monospace-font-size, 12px); 4 | line-height: var(--discovery-monospace-line-height, 1.5); 5 | } 6 | -------------------------------------------------------------------------------- /src/views/text-render.js: -------------------------------------------------------------------------------- 1 | import usage from './text-render.usage.js'; 2 | 3 | export default function(host) { 4 | host.view.define('text-render', async function(el, config, data, context) { 5 | const { content } = config; 6 | const textRenderTree = await host.textView.render('block', content, data, context); 7 | 8 | el.append(host.textView.serialize(textRenderTree).text); 9 | }, { tag: 'pre', usage }); 10 | 11 | host.view.define('text-render-tree', async function(el, config, data, context) { 12 | const { content, expanded = 2 } = config; 13 | const textRenderTree = await host.textView.render('block', content, data, context); 14 | 15 | return this.render(el, { view: 'struct', expanded }, host.textView.cleanUpRenderTree(textRenderTree)); 16 | }, { tag: false }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/views/text-render.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeDemo: [ 3 | 'md:"The `text-render` view renders data as text using the `Model#textView` API. For a full list of available text views, refer to the [`text`](#views-showcase&render=text) section."' 4 | ], 5 | demo: { 6 | view: 'text-render', 7 | content: [ 8 | 'text:"hello!"', 9 | { 10 | view: 'ul', 11 | data: [1, 2, 3, 4], 12 | item: 'text:"item #" + $' 13 | }, 14 | { 15 | view: 'table', 16 | data: [ 17 | { name: 'foo', size: 1000 }, 18 | { name: 'bar', size: 123 }, 19 | { name: 'baz', size: 34555 } 20 | ] 21 | } 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/views/text/alerts.css: -------------------------------------------------------------------------------- 1 | .view-alert { 2 | position: relative; 3 | padding: .75rem 1.25rem; 4 | margin-bottom: 1rem; 5 | border: 1px solid transparent; 6 | border-radius: .25rem; 7 | 8 | color: #383d41; 9 | background-color: rgba(139, 143, 151, 0.25); 10 | border-color: rgba(152, 157, 165, 0.4); 11 | background-clip: padding-box; 12 | } 13 | .discovery-root-darkmode .view-alert { 14 | color: var(--discovery-color); 15 | } 16 | .view-alert:last-child { 17 | margin-bottom: 0; 18 | } 19 | 20 | .view-alert > :first-child { 21 | margin-top: 0; 22 | } 23 | 24 | .page > .view-alert:first-child, 25 | .page > .view-alert-success:first-child, 26 | .page > .view-alert-danger:first-child, 27 | .page > .view-alert-warning:first-child { 28 | margin-top: 15px; 29 | } 30 | 31 | .view-alert-primary { 32 | color: #004085; 33 | background-color: rgba(51, 151, 255, 0.25); 34 | border-color: rgba(77, 162, 255, 0.4); 35 | } 36 | .discovery-root-darkmode .view-alert-primary { 37 | color: #6a96c6; 38 | } 39 | 40 | .view-alert-success { 41 | color: #155724; 42 | background-color: rgba(83, 183, 107, 0.25); 43 | border-color: rgba(105, 192, 125, 0.4); 44 | } 45 | .discovery-root-darkmode .view-alert-success { 46 | color: #5fab70; 47 | } 48 | 49 | .view-alert-danger { 50 | color: #721c24; 51 | background-color: rgba(227, 95, 107, 0.25); 52 | border-color: rgba(230, 112, 125, 0.4); 53 | } 54 | .discovery-root-darkmode .view-alert-danger { 55 | color: #c7888e; 56 | } 57 | 58 | .view-alert-warning { 59 | color: #856404; 60 | background-color: rgba(255, 207, 55, 0.25); 61 | border-color: rgba(255, 212, 82, 0.4); 62 | } 63 | .discovery-root-darkmode .view-alert-warning { 64 | color: #bdab77; 65 | } 66 | -------------------------------------------------------------------------------- /src/views/text/alerts.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import usage from './alerts.usage.js'; 4 | 5 | export default function(host) { 6 | function render(el, config, data, context) { 7 | const { content = 'text' } = config; 8 | 9 | el.classList.add('view-alert'); 10 | 11 | return host.view.render(el, content, data, context); 12 | } 13 | 14 | host.view.define('alert', render, { usage }); 15 | host.view.define('alert-primary', render, { usage }); 16 | host.view.define('alert-success', render, { usage }); 17 | host.view.define('alert-danger', render, { usage }); 18 | host.view.define('alert-warning', render, { usage }); 19 | } 20 | -------------------------------------------------------------------------------- /src/views/text/alerts.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: { 3 | view, 4 | data: '"Alert"' 5 | }, 6 | examples: [ 7 | { 8 | title: 'Variations', 9 | demo: group.map(name => `${name}:"${name}"`) 10 | }, 11 | { 12 | title: 'Complex content', 13 | demo: { 14 | view, 15 | content: [ 16 | 'h3:"Some header"', 17 | 'text:"Hello world!"' 18 | ] 19 | } 20 | } 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /src/views/text/app-header.css: -------------------------------------------------------------------------------- 1 | .view-app-header { 2 | display: flex; 3 | align-items: center; 4 | gap: 10px; 5 | } 6 | .view-app-header:not(:last-child) { 7 | margin-bottom: 30px; 8 | } 9 | .view-app-header .icon { 10 | --icon: var(--discovery-app-icon); 11 | 12 | float: left; 13 | display: inline-block; 14 | line-height: 1; 15 | 16 | width: 48px; 17 | aspect-ratio: 1; 18 | vertical-align: middle; 19 | padding: 4px; 20 | background: no-repeat center; 21 | background-image: var(--icon, var(--discovery-app-icon)); 22 | background-size: 48px; 23 | background-color: #f4f4f4; 24 | border-radius: 3px; 25 | } 26 | .discovery-root-darkmode .view-app-header .icon { 27 | background-color: #2b2b2b; 28 | } 29 | .view-app-header h1 { 30 | margin: 0; 31 | font-size: 30px; 32 | line-height: 31px; 33 | font-family: Helvetica Neue, Helvetica, Tahoma, Arial, sans-serif; 34 | font-weight: 200; 35 | } 36 | .view-app-header .version { 37 | vertical-align: top; 38 | display: inline-block; 39 | margin-left: 1ex; 40 | font-size: 12px; 41 | line-height: 18px; 42 | opacity: .4; 43 | } 44 | .view-app-header .version:hover { 45 | opacity: .75; 46 | } 47 | .view-app-header .description { 48 | font-size: 12px; 49 | line-height: 19px; 50 | color: #aaa; 51 | } 52 | -------------------------------------------------------------------------------- /src/views/text/app-header.js: -------------------------------------------------------------------------------- 1 | import { createElement } from '../../core/utils/index.js'; 2 | import usage from './app-header.usage.js'; 3 | 4 | const props = `is not array? | { 5 | name: name or "Untitled app", 6 | icon, 7 | version, 8 | description 9 | } | overrideProps() | { 10 | ..., 11 | icon is string and $ ~= /\\S/?, 12 | version is string and $ ~= /\\S/?, 13 | description is string and $ ~= /\\S/? 14 | }`; 15 | 16 | export default function(host) { 17 | host.view.define('app-header', function(el, props) { 18 | const { name, icon, version, description } = props; 19 | const headerEl = createElement('h1', null, version 20 | ? [name, createElement('span', 'version', version)] 21 | : [name] 22 | ); 23 | 24 | if (icon) { 25 | el.style.setProperty('--icon', /^(?:\.|\/|data:|https?:)/.test(icon) 26 | ? `url(${JSON.stringify(icon)})` 27 | : icon 28 | ); 29 | } 30 | 31 | el.append( 32 | createElement('div', 'icon'), 33 | createElement('div', 'content', description 34 | ? [headerEl, createElement('div', 'description', description)] 35 | : [headerEl] 36 | ) 37 | ); 38 | }, { props, usage }); 39 | } 40 | -------------------------------------------------------------------------------- /src/views/text/app-header.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'app-header', 4 | name: 'My app', 5 | version: '1.2.3', 6 | description: 'The best of the best app' 7 | }, 8 | examples: [ 9 | { 10 | title: 'Variations', 11 | demo: [ 12 | 'app-header', 13 | 'app-header{ name: "Only name" }', 14 | 'app-header{ name: "Name and version", version: "1.2.3" }', 15 | 'app-header{ name: "Name and description", description: "Some description" }', 16 | 'app-header{ name: "Everything set", version: "1.2.3", description: "Very useful description" }' 17 | ] 18 | } 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /src/views/text/auto-link.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default function(host) { 4 | host.view.define('auto-link', function(el, config, data, context) { 5 | const { content, fallback, href } = config; 6 | 7 | if (!data) { 8 | return; 9 | } 10 | 11 | const links = host.resolveValueLinks(data); 12 | const preprocessHref = typeof href === 'function' ? href : value => value; 13 | const processedHref = links 14 | ? preprocessHref(links[0].href, data, context) 15 | : null; 16 | 17 | if (processedHref) { 18 | return host.view.render(el, { view: 'link', content }, { 19 | ...links[0], 20 | href: processedHref 21 | }, context); 22 | } else { 23 | return host.view.render(el, fallback || content || 'text', data, context); 24 | } 25 | }, { 26 | tag: false 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/views/text/badges.css: -------------------------------------------------------------------------------- 1 | .view-badge, 2 | .view-pill-badge { 3 | --discovery-view-badge-color: rgba(135, 135, 135, 0.2); 4 | --discovery-view-badge-text-color: var(--discovery-color); 5 | padding: 1px 6px 3px; 6 | margin-right: 1ex; 7 | text-shadow: 1px 1px rgba(255, 255, 255, .35); 8 | background: var(--discovery-view-badge-color); 9 | color: var(--discovery-view-badge-text-color); 10 | border-radius: 3px; 11 | font-size: 85%; 12 | text-decoration: none; 13 | white-space: nowrap; 14 | } 15 | .view-pill-badge { 16 | border-radius: 9px; 17 | } 18 | .discovery-root-darkmode .view-badge, 19 | .discovery-root-darkmode .view-pill-badge { 20 | background: var(--discovery-view-badge-dark-color, var(--discovery-view-badge-color)); 21 | color: var(--discovery-view-badge-dark-text-color, var(--discovery-view-badge-text-color)); 22 | text-shadow: 1px 1px rgba(0, 0, 0, .1); 23 | } 24 | 25 | .view-badge[href]:hover, 26 | .view-badge.onclick:hover, 27 | .view-pill-badge[href]:hover, 28 | .view-pill-badge.onclick:hover { 29 | color: var(--discovery-color); 30 | background-image: linear-gradient(to top, rgba(0, 0, 0, .1), rgba(0, 0, 0, .1)); 31 | cursor: pointer; 32 | } 33 | 34 | .view-badge > .prefix { 35 | padding: 1px 6px 3px; 36 | margin: -1px 6px -3px -6px; 37 | border-radius: 3px 0 0 3px; 38 | background-color: rgba(0, 0, 0, .12); 39 | } 40 | .view-badge > .postfix { 41 | padding: 1px 6px 3px; 42 | margin: -1px -6px -3px 6px; 43 | border-radius: 0 3px 3px 0; 44 | background-color: rgba(0, 0, 0, .12); 45 | } 46 | .view-pill-badge > .prefix { 47 | padding: 1px 6px 3px; 48 | margin: -1px 6px -3px -6px; 49 | border-radius: 8px 0 0 8px; 50 | background-color: rgba(0, 0, 0, .12); 51 | } 52 | .view-pill-badge > .postfix { 53 | padding: 1px 6px 3px; 54 | margin: -1px -6px -3px 6px; 55 | border-radius: 0 8px 8px 0; 56 | background-color: rgba(0, 0, 0, .12); 57 | } 58 | 59 | .discovery-buildin-view-tooltip .view-badge, 60 | .discovery-buildin-view-tooltip .view-pill-badge { 61 | display: inline-block; 62 | margin: 0 0 3px -5px; 63 | padding-top: 0; 64 | padding-bottom: 1px; 65 | line-height: 19px; 66 | } 67 | -------------------------------------------------------------------------------- /src/views/text/blockquote-caution.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/blockquote-important.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/blockquote-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/blockquote-tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/blockquote-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/blockquote.css: -------------------------------------------------------------------------------- 1 | .view-blockquote { 2 | margin: 1px 0; 3 | padding: 0; 4 | padding-left: 1em; 5 | border-left: .25em solid var(--color, rgba(191, 197, 197, 0.5)); 6 | } 7 | .view-blockquote:not([data-kind]) { 8 | color: #6a737d; 9 | } 10 | .discovery-root-darkmode .view-blockquote:not([data-kind]) { 11 | color: #808993; 12 | } 13 | 14 | .view-blockquote[data-kind] { 15 | padding-top: .4em; 16 | padding-bottom: .4em; 17 | } 18 | .view-blockquote[data-kind]::before { 19 | content: var(--blockquote-title-text, attr(data-kind)); 20 | display: block; 21 | padding-left: calc(1em + .9ex); 22 | margin-bottom: .2em; 23 | background-image: var(--blockquote-title-icon); 24 | background-size: 1.1em; 25 | background-repeat: no-repeat; 26 | background-position: left center; 27 | color: var(--blockquote-title-color); 28 | text-transform: capitalize; 29 | } 30 | 31 | .view-blockquote[data-kind="note"] { 32 | --color: #1f6feb; 33 | --blockquote-title-icon: url("./blockquote-note.svg"); 34 | --blockquote-title-color: #4493f8; 35 | } 36 | .view-blockquote[data-kind="tip"] { 37 | --color: #238636; 38 | --blockquote-title-icon: url("./blockquote-tip.svg"); 39 | --blockquote-title-color: #3fb950; 40 | } 41 | .view-blockquote[data-kind="important"] { 42 | --color: #8957e5; 43 | --blockquote-title-icon: url("./blockquote-important.svg"); 44 | --blockquote-title-color: #ab7df8; 45 | } 46 | .view-blockquote[data-kind="warning"] { 47 | --color: #9e6a03; 48 | --blockquote-title-icon: url("./blockquote-warning.svg"); 49 | --blockquote-title-color: #d29922; 50 | } 51 | .view-blockquote[data-kind="caution"] { 52 | --color: #da3633; 53 | --blockquote-title-icon: url("./blockquote-caution.svg"); 54 | --blockquote-title-color: #f85149; 55 | } 56 | -------------------------------------------------------------------------------- /src/views/text/blockquote.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import usage from './blockquote.usage.js'; 4 | 5 | export default function(host) { 6 | function render(el, config, data, context) { 7 | const { content = 'text', kind } = config; 8 | 9 | if (typeof kind === 'string' && /\S/.test(kind)) { 10 | el.dataset.kind = kind.trim(); 11 | } 12 | 13 | return host.view.render(el, content, data, context); 14 | } 15 | 16 | host.view.define('blockquote', { 17 | tag: 'blockquote', 18 | render, 19 | usage 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/views/text/blockquote.usage.js: -------------------------------------------------------------------------------- 1 | export default (view) => ({ 2 | demo: { 3 | view, 4 | content: [ 5 | 'text:"Blockquote text..."', 6 | 'blockquote:"Nested blockquote\\nmulti-line text..."' 7 | ] 8 | }, 9 | examples: [ 10 | { 11 | title: 'Variations', 12 | demoData: 'Blockquote text...', 13 | demo: [ 14 | { kind: '', text: 'Default blockquote without any kind.' }, 15 | { kind: 'note', text: 'Useful information that users should know, even when skimming content.' }, 16 | { kind: 'tip', text: 'Helpful advice for doing things better or more easily.' }, 17 | { kind: 'important', text: 'Key information users need to know to achieve their goal.' }, 18 | { kind: 'warning', text: 'Urgent info that needs immediate user attention to avoid problems.' }, 19 | { kind: 'caution', text: 'Advises about risks or negative outcomes of certain actions.' } 20 | ] 21 | .map(({ kind, text }) => kind 22 | ? `${view}{ kind: "${kind}", data: ${JSON.stringify(text)} }` 23 | : `${view}:${JSON.stringify(text)}` 24 | ) 25 | } 26 | ] 27 | }); 28 | -------------------------------------------------------------------------------- /src/views/text/headers-anchor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/text/headers.css: -------------------------------------------------------------------------------- 1 | .view-header { /* the same as h4 */ 2 | font-size: 120%; 3 | font-weight: normal; 4 | position: relative; 5 | } 6 | 7 | .view-header > .view-header__anchor { 8 | position: absolute; 9 | margin-left: -22px; 10 | display: inline-block; 11 | width: 22px; 12 | text-align: center; 13 | color: inherit; 14 | background: url(./headers-anchor.svg) center / 20px no-repeat content-box; 15 | padding-top: 2px; 16 | font-weight: normal; 17 | opacity: 0; 18 | transition: opacity .25s; 19 | } 20 | .view-header:hover > .view-header__anchor, 21 | .view-header:hover > .view-header__anchor { 22 | opacity: 1; 23 | } 24 | .view-header > .view-header__anchor::before { 25 | content: '§'; 26 | display: inline-block; 27 | width: 0; 28 | visibility: hidden; 29 | } 30 | 31 | .view-h1 { 32 | font-size: 220%; 33 | font-weight: normal; 34 | margin: .812em 0 .65em; 35 | } 36 | 37 | .view-h2 { 38 | font-size: 150%; 39 | font-weight: normal; 40 | margin: .78em 0; 41 | } 42 | 43 | .view-h3 { 44 | font-size: 135%; 45 | font-weight: normal; 46 | margin: .73em 0; 47 | } 48 | 49 | .view-h4 { 50 | font-size: 120%; 51 | font-weight: normal; 52 | margin: .73em 0; 53 | } 54 | 55 | .view-h5 { 56 | font-size: 110%; 57 | font-weight: normal; 58 | margin: .68em 0; 59 | } 60 | -------------------------------------------------------------------------------- /src/views/text/headers.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { slug as generateSlug } from 'github-slugger'; 3 | import { createElement } from '../../core/utils/dom.js'; 4 | import usage from './headers.usage.js'; 5 | 6 | export default function(host) { 7 | async function render(el, config, data, context) { 8 | const { content, anchor = false } = config; 9 | 10 | el.classList.add('view-header'); 11 | 12 | await host.view.render(el, content || 'text', data, context); 13 | 14 | if (anchor) { 15 | const slug = generateSlug(anchor === true ? el.textContent : String(anchor)); 16 | const href = host.encodePageHash( 17 | host.pageId, 18 | host.pageRef, 19 | host.pageParams, 20 | slug 21 | ); 22 | 23 | el.prepend(createElement('a', { 24 | class: 'view-header__anchor', 25 | id: `!anchor:${slug}`, 26 | href 27 | })); 28 | } 29 | } 30 | 31 | host.view.define('header', render, { tag: 'h4', usage }); 32 | host.view.define('h1', render, { tag: 'h1', usage }); 33 | host.view.define('h2', render, { tag: 'h2', usage }); 34 | host.view.define('h3', render, { tag: 'h3', usage }); 35 | host.view.define('h4', render, { tag: 'h4', usage }); 36 | host.view.define('h5', render, { tag: 'h5', usage }); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/text/headers.usage.js: -------------------------------------------------------------------------------- 1 | export default (view, group) => ({ 2 | demo: `${view}:"Header \\"${view}\\""`, 3 | examples: [ 4 | { 5 | title: 'Variations', 6 | view: group.map(view => `${view}:"Header \\"${view}\\""`) 7 | }, 8 | { 9 | title: 'Complex content', 10 | demo: { 11 | view, 12 | content: [ 13 | 'text:"Text "', 14 | 'link:{ text: "Link" }' 15 | ] 16 | } 17 | }, 18 | { 19 | title: 'Using anchor', 20 | demo: [ 21 | { 22 | view, 23 | anchor: 'foo', 24 | content: 'text:"Explicit value for an anchor"' 25 | }, 26 | { 27 | view, 28 | anchor: true, 29 | content: 'text:"Auto generated anchor based on text content of header"' 30 | } 31 | ] 32 | } 33 | ] 34 | }); 35 | -------------------------------------------------------------------------------- /src/views/text/html.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './html.usage.js'; 3 | 4 | export default function(host) { 5 | const buffer = document.createElement('div'); 6 | 7 | host.view.define('html', function(el, config, data) { 8 | buffer.innerHTML = data; 9 | el.append(...buffer.childNodes); 10 | }, { 11 | tag: null, 12 | usage 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/views/text/html.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'html', 4 | data: '"

I am inner HTML

"' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/views/text/image-preview.css: -------------------------------------------------------------------------------- 1 | .view-image-preview { 2 | --image-preview-bg-color: rgba(119, 119, 119, 0.125); 3 | display: flex; 4 | justify-content: center; 5 | padding: 16px; 6 | background-image: 7 | linear-gradient(45deg, var(--image-preview-bg-color) 25%, transparent 0, transparent 75%, var(--image-preview-bg-color) 75%), 8 | linear-gradient(45deg, var(--image-preview-bg-color) 25%, transparent 0, transparent 75%, var(--image-preview-bg-color) 75%); 9 | background-position: 0 0, 10px 10px; 10 | background-size: 20px 20px; 11 | } 12 | 13 | .view-image-preview .view-image { 14 | /* background-color: transparent; */ 15 | opacity: 1; 16 | } 17 | -------------------------------------------------------------------------------- /src/views/text/image-preview.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import usage from './image-preview.usage.js'; 4 | 5 | export default function(host) { 6 | host.view.define('image-preview', function(el, config, data, context) { 7 | this.render(el, { 8 | view: 'image', 9 | ...config 10 | }, data, context); 11 | }, { usage }); 12 | } 13 | -------------------------------------------------------------------------------- /src/views/text/image-preview.usage.js: -------------------------------------------------------------------------------- 1 | import { demoImageSrc } from './image.usage.js'; 2 | 3 | export default { 4 | beforeDemo: ['md:"Similar to the `image` view but displays as a block with the image centered. The block has a checkered background to highlight image transparency."'], 5 | demo: { 6 | view: 'image-preview', 7 | src: demoImageSrc, 8 | height: 100 9 | }, 10 | examples: [ 11 | { 12 | title: 'Src is not defined', 13 | demo: 'image-preview' 14 | }, 15 | { 16 | title: 'Bad url', 17 | demo: 'image-preview{ src: "" }' 18 | } 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /src/views/text/image.css: -------------------------------------------------------------------------------- 1 | .view-image { 2 | background: no-repeat center / 20px; 3 | } 4 | .view-image:not(.error):not(.loaded) { 5 | background-color: rgba(141, 141, 141, .3); 6 | } 7 | 8 | .view-image.error, 9 | .view-image:not([src]) { 10 | width: 32px; 11 | height: 32px; 12 | background-image: url('./image.svg'); 13 | background-color: rgba(141, 70, 70, .3); 14 | } 15 | 16 | .discovery-root-darkmode .view-image { 17 | opacity: .85; 18 | } 19 | -------------------------------------------------------------------------------- /src/views/text/image.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './image.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('image', function(el, config) { 6 | Object.assign(el, config); 7 | el.onerror = () => el.classList.add('error'); 8 | el.onload = () => el.classList.add('loaded'); 9 | }, { tag: 'img', usage }); 10 | } 11 | -------------------------------------------------------------------------------- /src/views/text/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/text/indicator.css: -------------------------------------------------------------------------------- 1 | .view-indicator { 2 | display: inline-flex; 3 | margin: 0 1px 1px 0; 4 | width: 150px; 5 | height: 100px; 6 | background: rgba(181, 181, 181, 0.15); 7 | flex-direction: column; 8 | justify-content: center; 9 | text-align: center; 10 | text-decoration: none; 11 | } 12 | .view-indicator[href]:hover { 13 | background: rgba(165, 165, 165, 0.3); 14 | } 15 | 16 | .view-indicator > .value { 17 | color: #666; 18 | font-size: 40px; 19 | line-height: 1.2; 20 | } 21 | .view-indicator[href] > .value { 22 | color: #1f7ec5; 23 | } 24 | .view-indicator > .label { 25 | font-size: 14px; 26 | color: #888; 27 | box-sizing: border-box; 28 | padding: 0 8px; 29 | white-space: nowrap; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | -------------------------------------------------------------------------------- /src/views/text/indicator.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './indicator.usage.js'; 3 | 4 | export default function(host) { 5 | host.view.define('indicator', function(el, config, data, context) { 6 | const { value, label } = config; 7 | const { href } = data || {}; 8 | const valueEl = document.createElement('div'); 9 | const labelEl = document.createElement('div'); 10 | 11 | valueEl.className = 'value'; 12 | labelEl.className = 'label'; 13 | 14 | if (href) { 15 | el.href = href; 16 | } 17 | 18 | return Promise.all([ 19 | host.view.render(valueEl, value || 'text:value', data, context), 20 | host.view.render(labelEl, label || 'text:label', data, context) 21 | ]).then(() => el.append(valueEl, labelEl)); 22 | }, { 23 | tag: 'a', 24 | usage 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/views/text/indicator.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'indicator', 4 | data: { 5 | label: 'Label', 6 | value: '1234' 7 | } 8 | }, 9 | examples: [ 10 | { 11 | title: 'Indicator as link', 12 | demo: { 13 | view: 'indicator', 14 | data: { 15 | label: 'Label', 16 | value: '4321', 17 | href: '#' 18 | } 19 | } 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/text/link.css: -------------------------------------------------------------------------------- 1 | .view-link { 2 | color: var(--discovery-link-color, #0099DD); 3 | text-decoration-skip: ink; 4 | text-decoration-color: var(--discovery-link-underline-color, rgba(0, 153, 221, 0.4)); 5 | } 6 | .view-link.onclick { 7 | text-decoration-line: underline; 8 | cursor: pointer; 9 | } 10 | .view-link:hover { 11 | color: var(--discovery-link-hover-color, #0077BB); 12 | text-decoration-color: currentColor; 13 | } 14 | -------------------------------------------------------------------------------- /src/views/text/link.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './link.usage.js'; 3 | 4 | const props = `is not array? | { 5 | text: #.props.content is undefined ? is string ?: text, 6 | content: undefined, 7 | href, 8 | external, 9 | onClick: undefined 10 | } | overrideProps() | { 11 | $text; $href; 12 | ..., 13 | text: $text | is not undefined or no $href ?: $href, 14 | href: $href | is not undefined or no $text ?: $text 15 | }`; 16 | 17 | export default function(host) { 18 | host.view.define('link', function(el, props, data, context) { 19 | let { 20 | text, 21 | content, 22 | href, 23 | external, 24 | onClick 25 | } = props; 26 | 27 | if (href) { 28 | el.href = href; 29 | } 30 | 31 | if (external) { 32 | el.setAttribute('target', '_blank'); 33 | } 34 | 35 | if (typeof onClick === 'function') { 36 | el.classList.add('onclick'); 37 | el.addEventListener('click', (e) => { 38 | e.preventDefault(); 39 | onClick(el, data, context); 40 | }); 41 | } 42 | 43 | if (content) { 44 | return host.view.render(el, content, data, context); 45 | } else { 46 | el.textContent = text; 47 | } 48 | }, { 49 | tag: 'a', 50 | props, 51 | usage 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/views/text/markdown.css: -------------------------------------------------------------------------------- 1 | .view-markdown:first-child > :first-child { 2 | margin-top: 0; 3 | } 4 | .view-markdown:last-child > :last-child { 5 | margin-bottom: 0; 6 | } 7 | .view-markdown :is(p, blockquote, ul, ol, dl, pre, code) { 8 | margin: 0; 9 | } 10 | .view-markdown :is(p, blockquote, ul, ol, dl, table, pre):not(:first-child) { 11 | margin-top: 15px; 12 | } 13 | .view-markdown :is(ul, ol) + :is(ul, ol), 14 | .view-markdown li > :is(ul, ol) { 15 | margin-top: 0 !important; 16 | } 17 | 18 | .view-markdown code { 19 | padding: .2em .4em; 20 | font-family: var(--discovery-monospace-font-family); 21 | font-size: 90%; 22 | background-color: rgba(210, 220, 230, 0.2); 23 | border-radius: 3px; 24 | } 25 | .discovery-root-darkmode .view-markdown blockquote code { 26 | background-color: rgba(116, 126, 136, 0.2); 27 | } 28 | 29 | .view-markdown kbd { 30 | --border-color: #ddd; 31 | --bg-color: #f8f8f8; 32 | padding: .15em .4em; 33 | font-family: var(--discovery-monospace-font-family); 34 | font-size: 90%; 35 | border: 1px solid var(--border-color, #888); 36 | border-radius: 6px; 37 | box-shadow: 0 -1px var(--border-color, #888) inset; 38 | background-color: var(--bg-color); 39 | } 40 | .discovery-root-darkmode .view-markdown kbd { 41 | --border-color: #484848; 42 | --bg-color: #181818; 43 | } 44 | 45 | .view-markdown sup { 46 | vertical-align: top; 47 | } 48 | .view-markdown sub { 49 | vertical-align: bottom; 50 | } 51 | 52 | .view-markdown pre:not(.view-source) { 53 | overflow: auto; 54 | font-family: var(--discovery-monospace-font-family); 55 | font-size: 90%; 56 | line-height: 1.25; 57 | background-color: rgba(155, 155, 155, 0.1); 58 | border-radius: 3px; 59 | word-break: normal; 60 | } 61 | .view-markdown pre:not(.view-source) code { 62 | padding: 0; 63 | font-family: inherit; 64 | font-size: 100%; 65 | background: none; 66 | border-radius: 0; 67 | text-shadow: none; 68 | color: inherit; 69 | } 70 | 71 | .view-markdown .check-list-item { 72 | list-style: none; 73 | } 74 | .view-markdown .check-list-item > .view-checkbox:first-child { 75 | width: 20px; 76 | margin-left: -20px; 77 | } 78 | -------------------------------------------------------------------------------- /src/views/text/source-copied.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/views/text/source-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/text/text-match.css: -------------------------------------------------------------------------------- 1 | .view-text-match { 2 | background: rgba(255, 232, 5, 0.22); 3 | border-bottom: 2px solid rgba(213, 190, 15, 0.8); 4 | line-height: 1.2; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/text/text-match.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createElement } from '../../core/utils/dom.js'; 3 | import { matchAll } from '../../core/utils/pattern.js'; 4 | import usage from './text-match.usage.js'; 5 | 6 | const matchWrapperEl = createElement('span', 'view-text-match'); 7 | const props = `is not array? | { 8 | text: #.props has no 'text' ? text, 9 | match, 10 | ignoreCase: ignoreCase or false 11 | } | overrideProps()`; 12 | 13 | export default function(host) { 14 | host.view.define('text-match', function(el, props) { 15 | const { 16 | text, 17 | match: pattern, 18 | ignoreCase 19 | } = props; 20 | 21 | matchAll( 22 | String(text), 23 | pattern, 24 | text => el 25 | .append(text), 26 | text => el 27 | .appendChild(matchWrapperEl.cloneNode()) 28 | .append(text), 29 | ignoreCase 30 | ); 31 | }, { 32 | tag: false, 33 | props, 34 | usage 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/views/text/text-numeric.css: -------------------------------------------------------------------------------- 1 | .view-text-numeric .num-delim { 2 | padding-left: 0.14em; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/text/text-numeric.js: -------------------------------------------------------------------------------- 1 | import { numDelim } from '../../core/utils/html.js'; 2 | import usage from './text-numeric.usage.js'; 3 | 4 | const props = `{ 5 | text: #.props has no 'text'? 6 | } | overrideProps()`; 7 | 8 | export default function(host) { 9 | host.view.define('text-numeric', function (el, { text }) { 10 | el.innerHTML = numDelim(text); 11 | }, { 12 | tag: 'span', 13 | props, 14 | usage 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/views/text/text-numeric.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'text-numeric', 4 | data: '"Like a `text` view but adds a thousands separator to integer part of numbers, e.g. 12345678 or 12345.67890"' 5 | }, 6 | examples: [ 7 | { 8 | title: 'Shorthand usage', 9 | view: 'text-numeric:1234567' 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /src/views/text/text.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import usage from './text.usage.js'; 3 | 4 | const props = `{ 5 | text: #.props has no 'text'? 6 | } | overrideProps()`; 7 | 8 | export default function(host) { 9 | host.view.define('text', function(el, { text }) { 10 | el.appendChild(document.createTextNode(String(text))); 11 | }, { 12 | tag: false, 13 | props, 14 | usage 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/views/text/text.usage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | demo: { 3 | view: 'text', 4 | data: '"Hello world!"' 5 | }, 6 | examples: [ 7 | { 8 | title: 'Shorthand usage', 9 | view: 'text:"Hello world!"' 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /src/views/tree/tree.css: -------------------------------------------------------------------------------- 1 | .view-tree { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | font-size: 13px; 6 | line-height: 24px; 7 | } 8 | .view-tree:empty::before { 9 | content: attr(emptyText); 10 | color: #888; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "lib": ["ES2022", "DOM", "DOM.iterable"], 7 | "moduleResolution": "Bundler", 8 | "module": "ESNext", 9 | "target": "ES2022", 10 | "allowJs": true, 11 | "outDir": "./lib", // there are errors without the option (a value doesn't make sense) because of allowJs: true 12 | // "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "declaration": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------