├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── curvenote.ts ├── images └── tangle.gif ├── index.css ├── index.html ├── package.json ├── src ├── components │ ├── action.ts │ ├── base.ts │ ├── button.ts │ ├── checkbox.ts │ ├── display.ts │ ├── dynamic.ts │ ├── index.ts │ ├── input.ts │ ├── radio.ts │ ├── range.ts │ ├── select.ts │ ├── switch.ts │ ├── utils.ts │ ├── variable.ts │ └── visible.ts ├── index.css ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb-typescript", 9 | "eslint:recommended", 10 | "plugin:jest/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2018, 20 | "sourceType": "module", 21 | "project": ["./tsconfig.json"] 22 | }, 23 | "ignorePatterns": ["webpack.*"], 24 | "plugins": ["@typescript-eslint"], 25 | "rules": {}, 26 | "settings": { 27 | "import/resolver": { 28 | "node": { 29 | "extensions": [".js", ".ts"] 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, ci-*] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 12.x 17 | - uses: actions/cache@v2 18 | with: 19 | path: 'node_modules' 20 | key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} 21 | - name: install 22 | run: yarn install 23 | - name: lint 24 | run: yarn run lint 25 | - name: format 26 | run: yarn run lint:format 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Curvenote Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

curvenote.dev

2 | 3 | # @curvenote/components 4 | 5 | [![iooax/components on npm](https://img.shields.io/npm/v/@curvenote/components.svg)](https://www.npmjs.com/package/@curvenote/components) 6 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/curvenote/components/blob/master/LICENSE) 7 | [![Documentation](https://img.shields.io/badge/curvenote.dev-Docs-green)](https://curvenote.dev) 8 | 9 | The goal of components is to provide web-components for interactive scientific writing, reactive documents and [explorable explanations](https://explorabl.es). This library provides ways to create, update and display variables as dynamic text and modify them with buttons, inputs, sliders, switches, and dropdowns. 10 | 11 | The [curvenote/components](https://curvenote.dev) project is heavily inspired by [tangle.js](http://worrydream.com/Tangle/guide.html), re-imagined to use [web-components](https://www.webcomponents.org/)! 12 | This means you can declaratively write your variables and how to display them in `html` markup. 13 | To get an idea of what that looks like, let's take the canonical example of *Tangled Cookies* - a simple reactive document. 14 | 15 | ![How many calories in that cookie?](images/tangle.gif) 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

26 | When you eat cookies, 27 | you consume calories.
28 | That's of your recommended daily calories. 29 |

30 | ``` 31 | 32 | ## Getting Started 33 | 34 | Ink is based on web-components, which creates custom HTML tags so that they can make writing documents easier. 35 | To get started, copy the built javascript file to the head of your page: 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | You can also download the [latest release](https://github.com/curvenote/components/releases) from GitHub. If you are running this without a web server, ensure the script has `charset="utf-8"` in the script tag. You can also [install from npm](https://www.npmjs.com/package/@curvenote/components): 42 | 43 | ```bash 44 | >> npm install @curvenote/components 45 | ``` 46 | 47 | You should then be able to extend the package as you see fit: 48 | 49 | ```javascript 50 | import components from '@curvenote/components'; 51 | ``` 52 | 53 | Note that the npm module does not setup the [@curvenote/runtime](https://github.com/curvenote/runtime) store, nor does it register the components. See the [curvenote.ts](/curvenote.ts) file for what the built package does to `setup` the store and `register` the components. 54 | 55 | ## Basic Components 56 | 57 | * r-var 58 | * r-display 59 | * r-dynamic 60 | * r-range 61 | * r-action 62 | * r-button 63 | * r-switch 64 | * r-checkbox 65 | * r-radio 66 | * r-select 67 | * r-input 68 | * r-visible 69 | 70 | ## Documentation 71 | 72 | See https://curvenote.dev/components for full documentation. 73 | -------------------------------------------------------------------------------- /curvenote.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, combineReducers } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import runtime, { types } from '@curvenote/runtime'; 4 | import { register } from './src/components'; 5 | import './src/index.css'; 6 | import './index.css'; 7 | 8 | declare global { 9 | interface Window { 10 | curvenote: { 11 | store: types.Store; 12 | }; 13 | } 14 | } 15 | 16 | window.curvenote = { 17 | ...window.curvenote, 18 | store: createStore( 19 | combineReducers({ runtime: runtime.reducer }), 20 | applyMiddleware(thunkMiddleware, runtime.triggerEvaluate, runtime.dangerousEvaluatation), 21 | ) as types.Store, 22 | }; 23 | 24 | register(window.curvenote.store); 25 | -------------------------------------------------------------------------------- /images/tangle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvenote/components/928a919088f1cec36725115619038cd4376bfe4e/images/tangle.gif -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | body { 2 | --theme-primary: #e99f4b; 3 | --theme-secondary: #46f; 4 | } 5 | article { 6 | margin: auto; 7 | padding-left: 10px; 8 | padding-right: 250px; 9 | position: relative; 10 | width: calc(max(min(100vw, 790px), 500px)); 11 | } 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5 { 17 | font-family: 'Oswald', sans-serif; 18 | } 19 | p { 20 | display: block; 21 | text-align: justify; 22 | overflow-wrap: break-word; 23 | font-size: 17px; 24 | line-height: 1.8; 25 | margin-top: 20px; 26 | margin-bottom: 20px; 27 | color: #4d4d4d; 28 | font-family: 'Roboto', sans-serif; 29 | font-weight: 300; 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 8 | 9 |
10 |

@curvenote/components

11 |

12 | The goal of @curvenote/components is to provide web-components for interactive scientific writing, reactive documents and explorable explanations. 13 | Included in @curvenote/components are ways to create, update and display variables as text, inputs, sliders and switches. 14 |

15 | 16 |

Tangled Cookies

17 | 18 | 19 |

20 | @curvenote/components are heavily inspired by tangle.js, updated to be use web-components! 21 | This means you can declaratively write your variables and how to display them in html markup. 22 | To get an idea of what that looks like, let's take the canonical example of Tangled Cookies - a simple reactive document to encourage you to not eat more than 42 cookies in one day. 23 |

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

32 | When you eat cookies, 33 | you consume calories. 34 |

35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |

50 | When you eat cookies, 51 | you consume calories.
52 | That's of your recommended daily calories. 53 |

54 | 55 | 61 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | $\theta_1$:
82 | $\theta_2$: No Refraction Wave 83 | 84 | º 85 |
86 | $v_1$: 87 | m/s
88 | $v_2$: 89 | m/s
90 |
91 |

Test a range with step less than 1

92 | 93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@curvenote/components", 3 | "version": "0.3.4", 4 | "description": "Web components for interactive scientific writing, reactive documents and explorable explanations.", 5 | "main": "dist/index.js", 6 | "unpkg": "dist/curvenote.min.js", 7 | "types": "dist/index.d.ts", 8 | "keywords": [ 9 | "explorable explanations", 10 | "web components", 11 | "writing" 12 | ], 13 | "files": [ 14 | "dist" 15 | ], 16 | "author": "rowanc1", 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/curvenote/components.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/curvenote/components/issues" 27 | }, 28 | "homepage": "https://curvenote.dev/components", 29 | "scripts": { 30 | "test": "jest", 31 | "lint": "eslint \"src/**/*.ts\" -c .eslintrc.json", 32 | "lint:format": "prettier --check \"src/**/*.ts\"", 33 | "lint:format:fix": "prettier --write \"src/**/*.ts\"", 34 | "clean": "rm -rf dist", 35 | "link": "yarn unlink; yarn link; yarn link @curvenote/runtime", 36 | "start": "webpack serve --config webpack.dev.js", 37 | "build-dev": "webpack --config webpack.dev.js", 38 | "build": "webpack --config webpack.prod.js; tsc; cp src/index.css dist/curvenote.css; rm -rf dist/src", 39 | "prepublishOnly": "yarn run clean; yarn run build;" 40 | }, 41 | "dependencies": { 42 | "@curvenote/runtime": "^0.2.8", 43 | "d3-drag": "^3.0.0", 44 | "d3-format": "^3.1.0", 45 | "d3-selection": "^3.0.0", 46 | "lit-element": "^2.5.1", 47 | "lit-html": "^1.4.1", 48 | "lodash.throttle": "^4.1.1", 49 | "redux": "^4.1.2", 50 | "redux-thunk": "^2.4.1", 51 | "uuid": "^8.3.2" 52 | }, 53 | "devDependencies": { 54 | "@types/d3-drag": "^3.0.1", 55 | "@types/d3-format": "^3.0.1", 56 | "@types/d3-selection": "^3.0.2", 57 | "@types/jest": "^27.4.1", 58 | "@types/lodash": "^4.14.178", 59 | "@types/lodash.throttle": "^4.1.6", 60 | "@types/uuid": "^8.3.4", 61 | "@typescript-eslint/eslint-plugin": "^5.12.1", 62 | "clean-webpack-plugin": "^3.0.0", 63 | "css-loader": "^5.2.6", 64 | "eslint": "^7.29.0", 65 | "eslint-config-airbnb-typescript": "^12.3.1", 66 | "eslint-config-prettier": "^8.3.0", 67 | "eslint-plugin-import": "^2.23.4", 68 | "eslint-plugin-jest": "^24.3.6", 69 | "eslint-plugin-jsx-a11y": "^6.4.1", 70 | "eslint-plugin-prettier": "^3.4.0", 71 | "eslint-plugin-react": "^7.24.0", 72 | "eslint-plugin-react-hooks": "^4.2.0", 73 | "express": "^4.17.3", 74 | "html-webpack-plugin": "^5.3.2", 75 | "jest": "^27.5.1", 76 | "prettier": "^2.5.1", 77 | "style-loader": "^3.3.1", 78 | "ts-loader": "^9.2.6", 79 | "typescript": "^4.5.5", 80 | "webpack": "^5.40.0", 81 | "webpack-cli": "^4.7.2", 82 | "webpack-dev-server": "^3.11.2", 83 | "webpack-merge": "^5.8.0" 84 | }, 85 | "sideEffects": false 86 | } 87 | -------------------------------------------------------------------------------- /src/components/action.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | import { BaseComponent, withRuntime } from './base'; 3 | 4 | export const ActionSpec = { 5 | name: 'action', 6 | description: 'Inline text that has an action', 7 | properties: {}, 8 | events: { 9 | click: { args: [] }, 10 | hover: { args: ['enter'] }, 11 | }, 12 | }; 13 | 14 | @withRuntime(ActionSpec, { bind: { type: String, reflect: true } }) 15 | class Action extends BaseComponent { 16 | render() { 17 | const dispatch = (action: string, args: any[]) => this.$runtime?.dispatchEvent(action, args); 18 | this.classList[this.$runtime?.component?.events.click.func ? 'remove' : 'add']('noclick'); 19 | return html` dispatch('hover', [true])} 25 | @mouseleave=${() => dispatch('hover', [false])} 26 | >`; 28 | } 29 | } 30 | 31 | export default Action; 32 | -------------------------------------------------------------------------------- /src/components/base.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable max-classes-per-file */ 3 | import { LitElement, PropertyDeclaration, PropertyValues } from 'lit-element'; 4 | import { types, actions, selectors, DEFAULT_SCOPE, utils, provider } from '@curvenote/runtime'; 5 | import { Unsubscribe } from 'redux'; 6 | 7 | interface Constructable { 8 | new (...args: any): T; 9 | } 10 | 11 | export class BaseSubscribe extends LitElement { 12 | $runtime: any | null = null; 13 | 14 | #scope?: Element; 15 | 16 | public get scope(): string | null { 17 | const closestScope = this.closest('r-scope'); 18 | // Always use the *first* scope found. Important on removeVariable. 19 | if (closestScope != null) this.#scope = closestScope; 20 | return (this.#scope ?? closestScope)?.getAttribute('name') ?? DEFAULT_SCOPE; 21 | } 22 | 23 | disconnectedCallback() { 24 | super.disconnectedCallback(); 25 | this.$runtime?.remove(); 26 | } 27 | 28 | subscribe(id: string) { 29 | this.unsubscribe(); 30 | this.#unsubscribe = provider.subscribe(id, () => this.requestRuntimeUpdate()); 31 | return this.#unsubscribe; 32 | } 33 | 34 | requestRuntimeUpdate() { 35 | // Allows overwriting this! 36 | this.requestUpdate(); 37 | } 38 | 39 | #unsubscribe?: Unsubscribe; 40 | 41 | unsubscribe() { 42 | if (this.#unsubscribe) this.#unsubscribe(); 43 | } 44 | } 45 | 46 | export class BaseComponent extends BaseSubscribe { 47 | $runtime: types.ComponentShortcut<{ 48 | [P in keyof T['properties']]: T['properties'][P]['default']; 49 | }> | null = null; 50 | 51 | static spec: types.Spec | null = null; 52 | 53 | connectedCallback() { 54 | super.connectedCallback(); 55 | const { spec } = this.constructor as typeof BaseComponent; 56 | if (spec == null) return; 57 | const { scope, name } = this as any; 58 | const initializeProperties: Record = {}; 59 | const initializeEvents: Record = {}; 60 | Object.entries(spec.properties).forEach(([key, prop]) => { 61 | if (!prop.has.value && this.getAttribute(key)) { 62 | console.warn( 63 | `${this.tagName}: Property "${key}" is not defined, but attribute is provided.`, 64 | ); 65 | } 66 | if (!prop.has.func && this.getAttribute(`:${key}`)) { 67 | console.warn( 68 | `${this.tagName}: Property ":${key}" is not defined, but attribute is provided.`, 69 | ); 70 | } 71 | initializeProperties[key] = { 72 | name: key, 73 | value: prop.has.value ? this.getAttribute(prop.attribute) ?? prop.default : null, 74 | func: prop.has.func ? this.getAttribute(`:${prop.attribute}`) ?? '' : '', 75 | }; 76 | }); 77 | Object.entries(spec.events ?? {}).forEach(([key, evt]) => { 78 | initializeEvents[key] = { 79 | name: key, 80 | func: this.getAttribute(`:${evt.attribute}`) ?? '', 81 | }; 82 | }); 83 | const component = provider.dispatch( 84 | actions.createComponent(spec.name, initializeProperties, initializeEvents, { scope, name }), 85 | ); 86 | this.$runtime = component as unknown as types.ComponentShortcut<{ 87 | [P in keyof T['properties']]: T['properties'][P]['default']; 88 | }>; 89 | this.subscribe(this.$runtime.id); 90 | } 91 | } 92 | 93 | /* withRuntime 94 | 95 | A class wrapper intended for use with BaseComponent 96 | 97 | ``` 98 | @withRuntime(Spec) 99 | class MyComponent extends BaseComponent {...} 100 | ``` 101 | 102 | The wrapper inserts: 103 | * Getters and Setters for each: 104 | * property 105 | * these are `${prop}` 106 | * property Function 107 | * these are `${prop}Function` 108 | * event 109 | * these are `on${Prop}Event` (capital P) 110 | * `properties` (for lit-element) 111 | * Two for each in the property spec 112 | * `${prop}` 113 | * `${prop}Function` with the attribute `:${prop}` 114 | * One for each in the events spec 115 | * `on${Prop}Event` with the attribute `:${prop}` 116 | * static `spec` attribute 117 | */ 118 | export function withRuntime>>( 119 | specDefinition: T, 120 | additionalProperties: { [key: string]: PropertyDeclaration } = {}, 121 | ) { 122 | return (ComponentClass: C) => { 123 | const litProperties = { ...additionalProperties }; 124 | 125 | const spec = utils.getSpecFromDefinition(specDefinition); 126 | 127 | // Add the properties 128 | Object.entries(spec.properties).forEach(([key, prop]) => { 129 | if (!prop.has.value) { 130 | Object.defineProperty(ComponentClass.prototype, key, { 131 | get() { 132 | console.warn(`Property "${key}" is not defined for "${specDefinition.name}".`); 133 | return undefined; 134 | }, 135 | set() { 136 | console.warn(`Property "${key}" is not defined for "${specDefinition.name}".`); 137 | }, 138 | }); 139 | return; 140 | } 141 | litProperties[key] = { type: String, attribute: prop.attribute }; 142 | Object.defineProperty(ComponentClass.prototype, key, { 143 | get() { 144 | return this.$runtime?.state?.[key]; 145 | }, 146 | set(value: string) { 147 | if (value == null) { 148 | this.removeAttribute(prop.attribute); 149 | const prevFunc = this.$runtime?.component.properties[key].func; 150 | this.$runtime?.setProperties({ 151 | [key]: { value: value ?? prop.default, func: prevFunc }, 152 | }); 153 | } else { 154 | this.setAttribute(prop.attribute, String(value)); 155 | this.removeAttribute(`:${prop.attribute}`); 156 | this.$runtime?.setProperties({ [key]: { value: value ?? prop.default, func: '' } }); 157 | } 158 | }, 159 | }); 160 | }); 161 | 162 | // Add the property functions 163 | Object.entries(spec.properties).forEach(([key, prop]) => { 164 | if (!prop.has.func) return; 165 | litProperties[`${key}Function`] = { type: String, attribute: `:${prop.attribute}` }; 166 | Object.defineProperty(ComponentClass.prototype, `${key}Function`, { 167 | get() { 168 | return this.$runtime?.component.properties[key].func; 169 | }, 170 | set(value: string) { 171 | if (value == null) { 172 | this.removeAttribute(`:${prop.attribute}`); 173 | } else { 174 | this.setAttribute(`:${prop.attribute}`, String(value).trim()); 175 | } 176 | const prevValue = this.$runtime?.component.properties[key].value; 177 | this.$runtime?.setProperties({ 178 | [key]: { value: prevValue, func: String(value ?? '').trim() }, 179 | }); 180 | }, 181 | }); 182 | }); 183 | 184 | Object.entries(spec.events ?? {}).forEach(([key, evt]) => { 185 | // Add the property 186 | const onKeyEvent = `on${key.slice(0, 1).toUpperCase()}${key.slice(1)}Event`; 187 | litProperties[onKeyEvent] = { type: String, attribute: `:${evt.attribute}` }; 188 | 189 | Object.defineProperty(ComponentClass.prototype, onKeyEvent, { 190 | get() { 191 | return this.$runtime?.component.events[key].func; 192 | }, 193 | set(value: string) { 194 | if (value == null) { 195 | this.removeAttribute(`:${evt.attribute}`); 196 | this.$runtime?.set({}, { [key]: { func: String(value).trim() ?? '' } }); 197 | } else { 198 | this.setAttribute(`:${evt.attribute}`, String(value).trim()); 199 | this.$runtime?.set({}, { [key]: { func: String(value).trim() ?? '' } }); 200 | } 201 | }, 202 | }); 203 | }); 204 | 205 | Object.defineProperty(ComponentClass, 'properties', { 206 | get() { 207 | return litProperties; 208 | }, 209 | }); 210 | 211 | Object.defineProperty(ComponentClass, 'spec', { 212 | get() { 213 | return spec; 214 | }, 215 | }); 216 | }; 217 | } 218 | 219 | export function onBindChange( 220 | updated: PropertyValues, 221 | component: BaseComponent, 222 | eventKey?: string, 223 | ) { 224 | if (!updated.has('bind')) return; 225 | const { bind } = component as any; 226 | if (bind == null || bind === '') return; 227 | 228 | const { spec } = component.constructor as typeof BaseComponent; 229 | const variable = selectors.getVariableByName(provider.getState(), `${component.scope}.${bind}`); 230 | const props: any = { 231 | value: { value: null, func: bind }, 232 | }; 233 | if ('format' in spec!.properties) { 234 | props.format = { value: variable?.format ?? spec!.properties.format.default }; 235 | } 236 | const events = eventKey ? { [eventKey]: { func: `{${bind}: value}` } } : {}; 237 | component.$runtime?.set(props, events); 238 | } 239 | -------------------------------------------------------------------------------- /src/components/button.ts: -------------------------------------------------------------------------------- 1 | import { html, css } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime } from './base'; 4 | 5 | export const ButtonSpec = { 6 | name: 'button', 7 | description: 'Input button element', 8 | properties: { 9 | label: { type: types.PropTypes.string, default: 'Click Here' }, 10 | disabled: { type: types.PropTypes.boolean, default: false }, 11 | }, 12 | events: { 13 | click: { args: [] }, 14 | }, 15 | }; 16 | 17 | const litProps = { 18 | dense: { type: Boolean, reflect: true }, 19 | }; 20 | 21 | @withRuntime(ButtonSpec, litProps) 22 | class Button extends BaseComponent { 23 | dense = false; 24 | 25 | render() { 26 | const { label, disabled } = this.$runtime!.state; 27 | const { dense } = this; 28 | return html``; 36 | } 37 | 38 | static get styles() { 39 | return css` 40 | button { 41 | cursor: pointer; 42 | padding: 5px; 43 | white-space: normal; 44 | user-select: none; 45 | } 46 | button.dense { 47 | padding: 0px; 48 | } 49 | `; 50 | } 51 | } 52 | 53 | export default Button; 54 | -------------------------------------------------------------------------------- /src/components/checkbox.ts: -------------------------------------------------------------------------------- 1 | import { css, html, PropertyValues } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime, onBindChange } from './base'; 4 | import { HTMLElementEvent } from '../types'; 5 | 6 | export const CheckboxSpec = { 7 | name: 'checkbox', 8 | description: 'Inline text that drags a value inside a range', 9 | properties: { 10 | value: { type: types.PropTypes.boolean, default: false }, 11 | label: { type: types.PropTypes.string, default: '' }, 12 | }, 13 | events: { 14 | change: { args: ['value'] }, 15 | }, 16 | }; 17 | 18 | @withRuntime(CheckboxSpec, { bind: { type: String, reflect: true } }) 19 | class Checkbox extends BaseComponent { 20 | updated(updated: PropertyValues) { 21 | onBindChange(updated, this, 'change'); 22 | } 23 | 24 | static get styles() { 25 | return css` 26 | label, 27 | input { 28 | cursor: pointer; 29 | } 30 | `; 31 | } 32 | 33 | render() { 34 | const { value, label } = this.$runtime!.state; 35 | const change = (evt: HTMLElementEvent) => { 36 | this.$runtime?.dispatchEvent('change', [evt.target.checked]); 37 | }; 38 | 39 | return html` 46 | `; 47 | } 48 | } 49 | 50 | export default Checkbox; 51 | -------------------------------------------------------------------------------- /src/components/display.ts: -------------------------------------------------------------------------------- 1 | import { html, PropertyValues } from 'lit-element'; 2 | import { types, DEFAULT_FORMAT } from '@curvenote/runtime'; 3 | import { formatter } from '../utils'; 4 | import { BaseComponent, withRuntime, onBindChange } from './base'; 5 | import { getValueOrTransform } from './utils'; 6 | 7 | export const DisplaySpec = { 8 | name: 'display', 9 | description: 'Inline display of values', 10 | properties: { 11 | value: { type: types.PropTypes.number, default: NaN, description: 'Value of the display' }, 12 | format: { 13 | type: types.PropTypes.string, 14 | default: DEFAULT_FORMAT, 15 | description: 'Format of the variable', 16 | }, 17 | transform: { 18 | type: types.PropTypes.string, 19 | default: '', 20 | args: ['value'], 21 | has: { func: true, value: false }, 22 | }, 23 | }, 24 | events: {}, 25 | }; 26 | 27 | @withRuntime(DisplaySpec, { bind: { type: String, reflect: true } }) 28 | class Display extends BaseComponent { 29 | updated(updated: PropertyValues) { 30 | onBindChange(updated, this); 31 | } 32 | 33 | render() { 34 | const { format } = this.$runtime!.state; 35 | const val = getValueOrTransform(this.$runtime); 36 | const formatted = formatter(val, format); 37 | this.textContent = formatted; 38 | return html``; 39 | } 40 | } 41 | 42 | export default Display; 43 | -------------------------------------------------------------------------------- /src/components/dynamic.ts: -------------------------------------------------------------------------------- 1 | import { html, PropertyValues, css } from 'lit-element'; 2 | import { drag, DragBehavior } from 'd3-drag'; 3 | import { select } from 'd3-selection'; 4 | import { types, DEFAULT_FORMAT } from '@curvenote/runtime'; 5 | import throttle from 'lodash.throttle'; 6 | import { THROTTLE_SKIP } from '../types'; 7 | import { BaseComponent, withRuntime, onBindChange } from './base'; 8 | import { formatter } from '../utils'; 9 | import { getValueOrTransform } from './utils'; 10 | 11 | const CURSOR_COL_RESIZE = 'cursor-col-resize'; 12 | // The virtual width of the dynamic text, about the width of half a phone: 13 | const RANGE_WIDTH = 250; 14 | 15 | export const DynamicSpec = { 16 | name: 'dynamic', 17 | description: 'Inline text that drags a value inside a range', 18 | properties: { 19 | value: { type: types.PropTypes.number, default: 0 }, 20 | min: { type: types.PropTypes.number, default: 0 }, 21 | max: { type: types.PropTypes.number, default: 100 }, 22 | step: { type: types.PropTypes.number, default: 1 }, 23 | sensitivity: { 24 | type: types.PropTypes.number, 25 | default: 1, 26 | description: 'Higher the sensitivity, the faster the scroll.', 27 | }, 28 | format: { type: types.PropTypes.string, default: DEFAULT_FORMAT }, 29 | periodic: { type: types.PropTypes.boolean, default: false }, 30 | after: { 31 | type: types.PropTypes.string, 32 | default: '', 33 | description: 'Text to follow the formatted value, which remains dynamic.', 34 | }, 35 | transform: { 36 | type: types.PropTypes.string, 37 | default: '', 38 | args: ['value'], 39 | has: { func: true, value: false }, 40 | }, 41 | }, 42 | events: { 43 | change: { args: ['value'] }, 44 | }, 45 | }; 46 | 47 | function positiveModulus(n: number, m: number) { 48 | return ((n % m) + m) % m; 49 | } 50 | 51 | @withRuntime(DynamicSpec, { bind: { type: String, reflect: true } }) 52 | class Dynamic extends BaseComponent { 53 | updated(updated: PropertyValues) { 54 | onBindChange(updated, this, 'change'); 55 | } 56 | 57 | #dragging: boolean = false; 58 | 59 | #drag: DragBehavior | null = null; 60 | 61 | #prevValue: number = 0; 62 | 63 | firstUpdated(changedProps: PropertyValues) { 64 | super.firstUpdated(changedProps); 65 | 66 | // Set innerText if it is there to the after property: 67 | if (this.innerText) this.$runtime?.set({ after: { value: ` ${this.innerText}` } }); 68 | 69 | const throttled = throttle( 70 | (val: number) => this.$runtime?.dispatchEvent('change', [val]), 71 | THROTTLE_SKIP, 72 | ); 73 | 74 | const node = this as Element; 75 | const bodyClassList = document.getElementsByTagName('BODY')[0].classList; 76 | 77 | this.#drag = drag() 78 | .on('start', (event) => { 79 | event.sourceEvent.preventDefault(); 80 | event.sourceEvent.stopPropagation(); 81 | this.#dragging = true; // Hides the "drag" tool-tip 82 | const { value } = this.$runtime!.state; 83 | this.#prevValue = Number(value); // Start out with the actual value 84 | bodyClassList.add(CURSOR_COL_RESIZE); 85 | }) 86 | .on('end', () => { 87 | this.#dragging = false; 88 | bodyClassList.remove(CURSOR_COL_RESIZE); 89 | this.requestUpdate(); 90 | }) 91 | .on('drag', (event) => { 92 | event.sourceEvent.preventDefault(); 93 | event.sourceEvent.stopPropagation(); 94 | 95 | const { dx } = event; 96 | 97 | const { step, min, max, sensitivity, periodic } = this.$runtime!.state; 98 | 99 | // The sensitivity is based on the RANGE_WIDTH 100 | const valuePerPixel = sensitivity / (RANGE_WIDTH / (Math.abs(max - min) + 1)); 101 | 102 | let newValue; 103 | if (periodic) { 104 | newValue = positiveModulus(this.#prevValue + dx * valuePerPixel - min, max - min) + min; 105 | } else { 106 | newValue = Math.max(Math.min(this.#prevValue + dx * valuePerPixel, max), min); 107 | } 108 | // Store the actual value so the drag is smooth 109 | this.#prevValue = newValue; 110 | // Then round with the step size if it is greater than zero 111 | const val = step > 0 ? Math.round(newValue / step) * step : newValue; 112 | throttled(val); 113 | }); 114 | 115 | this.#drag(select(node)); 116 | } 117 | 118 | static get styles() { 119 | return css` 120 | :host { 121 | display: inline-block; 122 | position: relative; 123 | white-space: normal; 124 | } 125 | .dynamic { 126 | cursor: col-resize; 127 | } 128 | .help { 129 | left: calc(50% - 13px); 130 | top: -5px; 131 | line-height: 9px; 132 | position: absolute; 133 | display: none; 134 | user-select: none; 135 | font-size: 9px; 136 | font-family: sans-serif; 137 | text-transform: uppercase; 138 | font-weight: 400; 139 | } 140 | :host(:hover) .help { 141 | display: block; 142 | } 143 | `; 144 | } 145 | 146 | render() { 147 | const { format, after } = this.$runtime!.state; 148 | const val = getValueOrTransform(this.$runtime); 149 | return html`${formatter(val, format)}${after} 150 |
drag
`; 151 | } 152 | } 153 | 154 | export default Dynamic; 155 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { actions, setup, provider, types } from '@curvenote/runtime'; 2 | import Variable from './variable'; 3 | import Display from './display'; 4 | import Range from './range'; 5 | import Dynamic from './dynamic'; 6 | import Action from './action'; 7 | import Button from './button'; 8 | import Switch from './switch'; 9 | import Checkbox from './checkbox'; 10 | import Radio from './radio'; 11 | import Select from './select'; 12 | import Input from './input'; 13 | import Visible from './visible'; 14 | 15 | export function registerComponent(name: string, component: any) { 16 | provider.dispatch( 17 | actions.createSpec(component.spec!.name, component.spec!.properties, component.spec!.events), 18 | ); 19 | customElements.define(name, component); 20 | } 21 | 22 | export const register = (store: types.Store) => { 23 | setup(store); 24 | customElements.define('r-var', Variable); 25 | registerComponent('r-display', Display); 26 | registerComponent('r-dynamic', Dynamic); 27 | registerComponent('r-range', Range); 28 | registerComponent('r-action', Action); 29 | registerComponent('r-button', Button); 30 | registerComponent('r-switch', Switch); 31 | registerComponent('r-checkbox', Checkbox); 32 | registerComponent('r-radio', Radio); 33 | registerComponent('r-select', Select); 34 | registerComponent('r-input', Input); 35 | registerComponent('r-visible', Visible); 36 | }; 37 | 38 | export default { 39 | Variable, 40 | Display, 41 | Dynamic, 42 | Range, 43 | Action, 44 | Button, 45 | Switch, 46 | Checkbox, 47 | Radio, 48 | Select, 49 | Input, 50 | Visible, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/input.ts: -------------------------------------------------------------------------------- 1 | import { html, PropertyValues } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { HTMLElementEvent } from '../types'; 4 | import { BaseComponent, withRuntime, onBindChange } from './base'; 5 | 6 | export const InputSpec = { 7 | name: 'input', 8 | description: 'Input text element', 9 | properties: { 10 | value: { type: types.PropTypes.string, default: '' }, 11 | label: { type: types.PropTypes.string, default: '' }, 12 | }, 13 | events: { 14 | change: { args: ['value'] }, 15 | }, 16 | }; 17 | 18 | @withRuntime(InputSpec, { bind: { type: String, reflect: true } }) 19 | class Input extends BaseComponent { 20 | updated(updated: PropertyValues) { 21 | onBindChange(updated, this, 'change'); 22 | } 23 | 24 | render() { 25 | const { value } = this.$runtime!.state; 26 | 27 | const changeHandler = (event: HTMLElementEvent) => { 28 | const newValue = event.target.value; 29 | this.$runtime?.dispatchEvent('change', [newValue]); 30 | }; 31 | 32 | return html``; 33 | } 34 | } 35 | 36 | export default Input; 37 | -------------------------------------------------------------------------------- /src/components/radio.ts: -------------------------------------------------------------------------------- 1 | import { html, css, PropertyValues } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime, onBindChange } from './base'; 4 | import { getLabelsAndValues } from './utils'; 5 | 6 | export const RadioSpec = { 7 | name: 'radio', 8 | description: 'Input button element', 9 | properties: { 10 | value: { type: types.PropTypes.string, default: '' }, 11 | labels: { type: types.PropTypes.string, default: '', description: 'Comma seperated values' }, 12 | values: { type: types.PropTypes.string, default: '', description: 'Comma seperated values' }, 13 | }, 14 | events: { 15 | change: { args: ['value'] }, 16 | }, 17 | }; 18 | 19 | @withRuntime(RadioSpec, { bind: { type: String, reflect: true } }) 20 | class Radio extends BaseComponent { 21 | updated(updated: PropertyValues) { 22 | onBindChange(updated, this, 'change'); 23 | } 24 | 25 | static get styles() { 26 | return css` 27 | label, 28 | input { 29 | cursor: pointer; 30 | } 31 | `; 32 | } 33 | 34 | render() { 35 | const { value, labels: labelsString, values: valuesString } = this.$runtime!.state; 36 | const { labels, values } = getLabelsAndValues(labelsString, valuesString); 37 | const changeHandler = (newValue: string) => () => { 38 | this.$runtime?.dispatchEvent('change', [newValue]); 39 | }; 40 | const name = this.$runtime!.id; 41 | return labels.map((label, i) => { 42 | const id = `${name}-${label}`; 43 | return html`

44 | 46 | 47 |

`; 48 | }); 49 | } 50 | } 51 | 52 | export default Radio; 53 | -------------------------------------------------------------------------------- /src/components/range.ts: -------------------------------------------------------------------------------- 1 | import { css, html, PropertyValues } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import throttle from 'lodash.throttle'; 4 | import { BaseComponent, withRuntime, onBindChange } from './base'; 5 | import { HTMLElementEvent, THROTTLE_SKIP } from '../types'; 6 | 7 | export const RangeSpec = { 8 | name: 'range', 9 | description: 'Range input', 10 | properties: { 11 | value: { type: types.PropTypes.number, default: 0 }, 12 | min: { type: types.PropTypes.number, default: 0 }, 13 | max: { type: types.PropTypes.number, default: 100 }, 14 | step: { type: types.PropTypes.number, default: 1 }, 15 | }, 16 | events: { 17 | change: { args: ['value'] }, 18 | }, 19 | }; 20 | 21 | @withRuntime(RangeSpec, { bind: { type: String, reflect: true } }) 22 | class Range extends BaseComponent { 23 | updated(updated: PropertyValues) { 24 | onBindChange(updated, this, 'change'); 25 | } 26 | 27 | #throttled: ((v: number) => void) | null = null; 28 | 29 | static get styles() { 30 | return css` 31 | input { 32 | cursor: pointer; 33 | } 34 | `; 35 | } 36 | 37 | render() { 38 | const { value, min, max, step } = this.$runtime!.state; 39 | 40 | if (this.#throttled == null) { 41 | this.#throttled = throttle( 42 | (val: number) => this.$runtime?.dispatchEvent('change', [val]), 43 | THROTTLE_SKIP, 44 | ); 45 | } 46 | 47 | const changeHandler = (event: HTMLElementEvent) => { 48 | const newValue = Number.parseFloat(event.target.value); 49 | this.#throttled!(newValue); 50 | }; 51 | 52 | return html``; 60 | } 61 | } 62 | 63 | export default Range; 64 | -------------------------------------------------------------------------------- /src/components/select.ts: -------------------------------------------------------------------------------- 1 | import { html, PropertyValues } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime, onBindChange } from './base'; 4 | import { getLabelsAndValues } from './utils'; 5 | 6 | export const SelectSpec = { 7 | name: 'select', 8 | description: 'Input button element', 9 | properties: { 10 | value: { type: types.PropTypes.string, default: '' }, 11 | label: { type: types.PropTypes.string, default: '' }, 12 | labels: { type: types.PropTypes.string, default: '', description: 'Comma seperated values' }, 13 | values: { type: types.PropTypes.string, default: '', description: 'Comma seperated values' }, 14 | }, 15 | events: { 16 | change: { args: ['value'] }, 17 | }, 18 | }; 19 | 20 | @withRuntime(SelectSpec, { bind: { type: String, reflect: true } }) 21 | class Select extends BaseComponent { 22 | updated(updated: PropertyValues) { 23 | onBindChange(updated, this, 'change'); 24 | } 25 | 26 | render() { 27 | const { value, labels: labelsString, values: valuesString } = this.$runtime!.state; 28 | const { labels, values } = getLabelsAndValues(labelsString, valuesString); 29 | const changeHandler = (evt: any) => { 30 | this.$runtime?.dispatchEvent('change', [evt.target.value]); 31 | }; 32 | 33 | return html``; 41 | } 42 | } 43 | 44 | export default Select; 45 | -------------------------------------------------------------------------------- /src/components/switch.ts: -------------------------------------------------------------------------------- 1 | import { html, PropertyValues, css } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime, onBindChange } from './base'; 4 | import { HTMLElementEvent } from '../types'; 5 | 6 | export const SwitchSpec = { 7 | name: 'switch', 8 | description: 'Inline text that drags a value inside a range', 9 | properties: { 10 | value: { type: types.PropTypes.boolean, default: false }, 11 | label: { type: types.PropTypes.string, default: '' }, 12 | }, 13 | events: { 14 | change: { args: ['value'] }, 15 | }, 16 | }; 17 | 18 | @withRuntime(SwitchSpec, { bind: { type: String, reflect: true } }) 19 | class Switch extends BaseComponent { 20 | updated(updated: PropertyValues) { 21 | onBindChange(updated, this, 'change'); 22 | } 23 | 24 | static get styles() { 25 | return css` 26 | :host { 27 | white-space: normal; 28 | user-select: none; 29 | } 30 | .switch { 31 | position: relative; 32 | display: inline-block; 33 | width: 36px; 34 | height: 22px; 35 | top: 3px; 36 | } 37 | input { 38 | opacity: 0; 39 | width: 0; 40 | height: 0; 41 | } 42 | .slider { 43 | position: absolute; 44 | cursor: pointer; 45 | top: 0; 46 | left: 0; 47 | right: 0; 48 | bottom: 0; 49 | background-color: #ccc; 50 | transition: 0.15s ease; 51 | border-radius: 30px; 52 | } 53 | .slider:before { 54 | position: absolute; 55 | content: ''; 56 | height: 14px; 57 | width: 14px; 58 | left: 4px; 59 | bottom: 4px; 60 | background-color: white; 61 | transition: 0.15s ease; 62 | border-radius: 50%; 63 | } 64 | input:checked + .slider { 65 | background-color: var(--theme-secondary, #46f); 66 | } 67 | .switch:hover .slider { 68 | filter: brightness(90%); 69 | } 70 | input:focus + .slider { 71 | box-shadow: 0 0 3px var(--theme-secondary, #46f); 72 | } 73 | input:checked + .slider:before { 74 | transform: translateX(14px); 75 | } 76 | .label { 77 | cursor: pointer; 78 | } 79 | `; 80 | } 81 | 82 | render() { 83 | const { value, label } = this.$runtime!.state; 84 | const change = (evt: HTMLElementEvent) => { 85 | this.$runtime?.dispatchEvent('change', [evt.target.checked]); 86 | }; 87 | 88 | return html` 98 | `; 99 | } 100 | } 101 | 102 | export default Switch; 103 | -------------------------------------------------------------------------------- /src/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@curvenote/runtime'; 2 | 3 | export function getLabelsAndValues(labelsString: string, valuesString: string) { 4 | const labels = 5 | labelsString === '' ? String(valuesString).split(',') : String(labelsString).split(','); 6 | const values = valuesString === '' ? labels : String(valuesString).split(','); 7 | 8 | if (labels.length !== values.length) { 9 | // eslint-disable-next-line no-console 10 | console.warn( 11 | `Labels and values do not match: labels: "${labelsString}" values: "${valuesString}"`, 12 | ); 13 | } 14 | return { labels, values }; 15 | } 16 | 17 | export function getValueOrTransform( 18 | $runtime: types.ComponentShortcut<{ transform: any; value: number }> | null, 19 | ) { 20 | const { transform, value } = $runtime!.state; 21 | const transformFunc = $runtime!.component?.properties.transform.func ?? ''; 22 | const useTransform = transformFunc !== '' && transformFunc?.trim() !== 'value'; 23 | const val = useTransform ? transform : value; 24 | return val; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/variable.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | import { actions, types, VariableSpec, provider } from '@curvenote/runtime'; 3 | import { formatter } from '../utils'; 4 | import { BaseSubscribe, withRuntime } from './base'; 5 | 6 | function toString(current: any, format: string) { 7 | switch (typeof current) { 8 | case 'string': 9 | return `"${current}"`; 10 | case 'number': 11 | return formatter(current, format); 12 | case 'object': 13 | try { 14 | return JSON.stringify(current); 15 | } catch (error) { 16 | return String(current); 17 | } 18 | default: 19 | return String(current); 20 | } 21 | } 22 | 23 | @withRuntime(VariableSpec) 24 | class Variable extends BaseSubscribe { 25 | connectedCallback() { 26 | super.connectedCallback(); 27 | const { scope } = this; 28 | const name = this.getAttribute('name') as string; 29 | this.$runtime = provider.dispatch( 30 | actions.createVariable( 31 | `${scope}.${name}`, 32 | this.getAttribute('value') ?? VariableSpec.properties.value.default, 33 | this.getAttribute(':value') ?? '', 34 | { 35 | description: this.getAttribute('description') ?? '', 36 | type: 37 | (this.getAttribute('type') as types.PropTypes) ?? VariableSpec.properties.type.default, 38 | format: 39 | this.getAttribute('format') ?? 40 | (VariableSpec.properties.format.default as types.PropTypes), 41 | }, 42 | ), 43 | ); 44 | this.subscribe(this.$runtime.id); 45 | } 46 | 47 | render() { 48 | const { name, value, current, func, format, derived } = this.$runtime!.state; 49 | // TODO: show error if name is not defined 50 | const currentStr = toString(current, format); 51 | if (derived) { 52 | return html`function ${name}() { return ${func}; } = ${currentStr}`; 53 | } 54 | const valueStr = toString(value, format); 55 | return html`${name} = ${valueStr}, Current: ${currentStr}`; 56 | } 57 | } 58 | 59 | export default Variable; 60 | -------------------------------------------------------------------------------- /src/components/visible.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | import { types } from '@curvenote/runtime'; 3 | import { BaseComponent, withRuntime } from './base'; 4 | 5 | export const VisibleSpec = { 6 | name: 'visible', 7 | description: 'Component that reacts to visibility', 8 | properties: { 9 | visible: { type: types.PropTypes.boolean, default: true, has: { value: false, func: true } }, 10 | }, 11 | events: {}, 12 | }; 13 | 14 | @withRuntime(VisibleSpec, { bind: { type: String, reflect: true } }) 15 | class Visible extends BaseComponent { 16 | render() { 17 | const { visible } = this.$runtime!.state; 18 | this.hidden = !visible; 19 | return html``; 20 | } 21 | } 22 | 23 | export default Visible; 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | r-var { 2 | display: block; 3 | color: #d46485; 4 | border: 1px solid #d4d4d4; 5 | border-radius: 2px; 6 | background-color: #efefef; 7 | width: fit-content; 8 | padding: 5px; 9 | user-select: none; 10 | } 11 | r-dynamic { 12 | color: var(--theme-secondary, #46f); 13 | border-bottom: 1px dashed var(--theme-secondary, #46f); 14 | } 15 | r-action { 16 | cursor: pointer; 17 | color: var(--theme-secondary, #46f); 18 | border-bottom: 1px solid var(--theme-secondary, #46f); 19 | } 20 | r-action.noclick { 21 | cursor: unset; 22 | border-bottom: unset; 23 | } 24 | .cursor-move { 25 | cursor: move; 26 | } 27 | .cursor-col-resize { 28 | cursor: col-resize; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css, svg, PropertyDeclaration, PropertyValues } from 'lit-element'; 2 | import { DEFAULT_FORMAT } from '@curvenote/runtime'; 3 | import { render as renderHTML } from 'lit-html'; 4 | import { unsafeHTML } from 'lit-html/directives/unsafe-html'; 5 | import throttle from 'lodash.throttle'; 6 | import { THROTTLE_SKIP } from './types'; 7 | import { BaseComponent, withRuntime } from './components/base'; 8 | import components, { register, registerComponent } from './components'; 9 | import { formatter } from './utils'; 10 | 11 | export { 12 | // Downstream components that use BaseComponent should use these LitElement instances 13 | // There are some singletons that get created by that library that make things hard 14 | LitElement, 15 | html, 16 | css, 17 | svg, 18 | PropertyDeclaration, 19 | PropertyValues, 20 | unsafeHTML, 21 | renderHTML, 22 | BaseComponent, 23 | withRuntime, 24 | register, 25 | registerComponent, 26 | throttle, 27 | THROTTLE_SKIP, 28 | DEFAULT_FORMAT, 29 | formatter, 30 | }; 31 | 32 | export default components; 33 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export const THROTTLE_SKIP = 100; 2 | 3 | export type HTMLElementEvent = Event & { 4 | target: T; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'd3-format'; 2 | import { types, DEFAULT_FORMAT } from '@curvenote/runtime'; 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export function formatter(value: any, formatString?: string, variable?: types.Variable) { 6 | if (typeof value === 'string') { 7 | return value; 8 | } 9 | if (typeof value === 'boolean') { 10 | return value ? 'true' : 'false'; 11 | } 12 | try { 13 | return format(formatString ?? variable?.format ?? DEFAULT_FORMAT)(value); 14 | } catch (error) { 15 | try { 16 | return format(DEFAULT_FORMAT)(value); 17 | } catch (error2) { 18 | return value; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "lib": [ 7 | "es2019", 8 | "dom" 9 | ], 10 | "noImplicitAny": true, 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "experimentalDecorators": true, 14 | "sourceMap": true, 15 | "outDir": "dist", 16 | "baseUrl": ".", 17 | "paths": { 18 | "*": [ 19 | "node_modules/*" 20 | ] 21 | }, 22 | "declaration": true, 23 | "skipLibCheck": true 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | // const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | // const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | optimization: { 8 | usedExports: true, 9 | }, 10 | entry: { 11 | app: './curvenote.ts', 12 | }, 13 | plugins: [ 14 | // new CleanWebpackPlugin(), 15 | new HtmlWebpackPlugin({ 16 | title: '@curvenote/components', 17 | template: 'index.html', 18 | }), 19 | ], 20 | output: { 21 | filename: 'curvenote.min.js', 22 | path: path.resolve(__dirname, 'dist'), 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | use: 'ts-loader', 29 | exclude: /node_modules/, 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | 'style-loader', 35 | 'css-loader', 36 | ], 37 | }, 38 | { 39 | test: /\.(png|svg|jpg|gif)$/, 40 | use: [ 41 | 'file-loader', 42 | ], 43 | }, 44 | ], 45 | }, 46 | resolve: { 47 | extensions: ['.tsx', '.ts', '.js'], 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge').default; 2 | const express = require('express'); 3 | const path = require('path'); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'development', 8 | devtool: 'inline-source-map', 9 | devServer: { 10 | contentBase: './dist', 11 | before(app) { 12 | app.use('/images', express.static(path.resolve('images'))); 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge').default; 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | }); 7 | --------------------------------------------------------------------------------