├── .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 |
2 |
3 | # @curvenote/components
4 |
5 | [](https://www.npmjs.com/package/@curvenote/components)
6 | [](https://github.com/curvenote/components/blob/master/LICENSE)
7 | [](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 | 
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` {
21 | e.preventDefault();
22 | dispatch('click', []);
23 | }}"
24 | @mouseenter=${() => 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` this.$runtime?.dispatchEvent('click')}"
33 | >
34 | ${label}
35 | `;
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 | ${label} `;
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 | ${label}
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`
34 | ${labels.map(
35 | (item, i) =>
36 | html`
37 | ${item}
38 | `,
39 | )}
40 | `;
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`
89 |
96 |
97 |
98 | ${label} `;
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 |
--------------------------------------------------------------------------------