├── .gitignore
├── .scripts
├── docs-examples.sh
├── docs-index.sh
└── docs-inject-ga.sh
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── Makefile
├── README.md
├── docs-src
├── ga.html.snippet
├── index.html.begin
├── index.html.end
└── index.md
├── docs
├── .gitignore
├── examples
│ ├── mortgage
│ │ ├── bundle.js
│ │ └── index.html
│ ├── pizza
│ │ ├── bundle.js
│ │ ├── index.html
│ │ └── styles.css
│ └── spreadsheet
│ │ ├── bundle.js
│ │ └── index.html
├── index.html
├── spreadsheet.png
└── style.css
├── examples
├── mortgage
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── index.js
│ │ ├── mortgage.js
│ │ └── utils.js
│ └── webpack.config.js
├── pizza
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── index.js
│ │ └── style.css
│ └── webpack.config.js
└── spreadsheet
│ ├── .babelrc
│ ├── .vscode
│ └── settings.json
│ ├── index.html
│ ├── package.json
│ ├── src
│ ├── cell-component.js
│ ├── compile-formula.js
│ ├── formula.js
│ ├── formula.test.js
│ ├── functions.js
│ ├── index.js
│ ├── sheet.js
│ └── spreadsheet.js
│ └── webpack.config.js
├── lerna.json
├── package.json
├── packages
├── xcell-inspect
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── cell-to-dot-node.ts
│ │ ├── create-dot.ts
│ │ ├── deferred.ts
│ │ ├── dot-layout.test.ts
│ │ ├── dot-layout.ts
│ │ ├── index-umd.ts
│ │ ├── index.ts
│ │ ├── index.worker.ts
│ │ ├── inspect.ts
│ │ ├── render-dot-graph.ts
│ │ ├── render-root.ts
│ │ ├── style.css
│ │ └── worker-functions.ts
│ ├── tsconfig.json
│ └── webpack.config.js
└── xcell
│ ├── .gitignore
│ ├── .npmignore
│ ├── README.md
│ ├── jest.debug.json
│ ├── package.json
│ ├── src
│ ├── cell.test.ts
│ ├── cell.ts
│ ├── index-umd.ts
│ └── index.ts
│ ├── tsconfig.debug.json
│ ├── tsconfig.json
│ └── webpack.config.js
├── tsconfig.common.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | yarn-error.log
4 |
--------------------------------------------------------------------------------
/.scripts/docs-examples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mkdir -p docs/examples
4 |
5 | for i in `ls examples`
6 | do
7 | pushd examples/$i
8 | npm run build&
9 | popd
10 | done
11 |
12 | wait
13 |
14 | for i in `ls examples`
15 | do
16 | mkdir -p docs/examples/$i
17 | cp -r examples/$i/dist/* docs/examples/$i
18 | mv docs/examples/$i/index.html docs/examples/$i/~index.html
19 | .scripts/docs-inject-ga.sh < docs/examples/$i/~index.html > docs/examples/$i/index.html
20 | rm docs/examples/$i/~index.html
21 | done
22 |
23 |
--------------------------------------------------------------------------------
/.scripts/docs-index.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | REMARK=./node_modules/.bin/remark
6 | REMARK_CMD="${REMARK} \
7 | -q \
8 | --use html \
9 | --use highlight.js \
10 | --use preset-lint-markdown-style-guide"
11 |
12 | cat docs-src/index.html.begin \
13 | <(${REMARK_CMD} docs-src/index.md) \
14 | docs-src/index.html.end
15 |
--------------------------------------------------------------------------------
/.scripts/docs-inject-ga.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | sed '/<\/head>/ {
6 | h
7 | r docs-src/ga.html.snippet
8 | g
9 | N
10 | }'
11 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "preLaunchTask": "xcell-build-for-debug",
11 | "name": "debug xcell",
12 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
13 | "args": [
14 | "--config",
15 | "${workspaceFolder}/packages/xcell/jest.debug.json",
16 | "--runInBand"
17 | ],
18 | "sourceMaps": true,
19 | "trace": true,
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "cSpell.words": [
4 | "debounced",
5 | "isequal",
6 | "lightgrey",
7 | "morhpdom",
8 | "readonly",
9 | "shallowequal",
10 | "xcell"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "xcell-build-for-debug",
8 | "type": "npm",
9 | "script": "xcell:build:for-debug",
10 | "problemMatcher": []
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all build docs docs-examples
2 |
3 | all: build docs
4 |
5 | build:
6 | npm run build
7 |
8 | docs: docs/index.html docs-examples
9 |
10 | docs/index.html: docs-src/index.md
11 | .scripts/docs-index.sh | .scripts/docs-inject-ga.sh > docs/index.html
12 |
13 | docs-examples:
14 | .scripts/docs-examples.sh
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xcell monorepo
2 |
3 | A a tiny library for building reactive spreadsheet-like calculations in JavaScript
4 |
5 | Packages:
6 | * [xcell](packages/xcell)
7 | * [xcell-inspect](packages/xcell-inspect)
8 |
9 | Check the [`examples`](examples) folder to see how it can be used.
10 |
11 | The examples are hosted here: https://tomazy.github.io/xcell/
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs-src/ga.html.snippet:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/docs-src/index.html.begin:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs-src/index.html.end:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs-src/index.md:
--------------------------------------------------------------------------------
1 | # xcell
2 |
3 | [xcell](https://github.com/tomazy/xcell) is a tiny, open source (MIT)
4 | library for building reactive, spreadsheet-like calculations in JavaScript.
5 |
6 | Spreadsheets are cool—if we say that **A1** is the sum of **B1** and **C1**,
7 | the spreadsheet will automatically update whenever we change the dependent
8 | cells.
9 |
10 | 
11 |
12 | This usually doesn't happen in our programs.
13 |
14 | For example in JavaScript:
15 |
16 | ```javascript
17 | var b = 1, c = 2
18 |
19 | var a = b + c // a is 3 now
20 |
21 | b = 42
22 |
23 | alert("a is now: " + a) // it is still 3 :(
24 | ```
25 |
26 | our variable **a** does not automatically change if we
27 | change **b**. It will be equal to **3** until we *imperatively*
28 | change it something else.
29 |
30 | **xcell** allows us to write programs that work like spreadsheets.
31 |
32 | Here is how:
33 |
34 | ```javascript
35 |
36 | function add(x, y) {
37 | return x + y
38 | }
39 |
40 | var b = xcell(1), c = xcell(2)
41 |
42 | var a = xcell([b, c], add)
43 |
44 | alert(a.value) // a is 3
45 |
46 | b.value = 42
47 |
48 | alert(a.value) // a is 44 \o/
49 | ```
50 |
51 | `xcell` is a function that returns an object that holds always
52 | updated value, just like a spreadsheet cell.
53 |
54 | When we create our "cells" we tell them to either be independent:
55 |
56 | ```javascript
57 | var b = xcell(1)
58 | ```
59 |
60 | or to depend on other cells and update its value when necessary using
61 | a provided function:
62 |
63 | ```javascript
64 | var a = xcell([b, c], add)
65 | ```
66 |
67 | The cells emit `change` event whenever they change, so we can observe
68 | them and update our UI:
69 |
70 | ```javascript
71 | a.on('change', function handleChange(sender) {
72 | document.getElementById("my-cell").value = sender.value
73 | })
74 | ```
75 |
76 | Here are a few examples of how **xcell** can be used:
77 |
78 | - [the real price of a pizza](examples/pizza)
79 | - [mortgage calculator](examples/mortgage)
80 | - [spreadsheet demo](examples/spreadsheet)
81 |
82 | The source code is on [github](https://github.com/tomazy/xcell).
83 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | *.map
2 |
--------------------------------------------------------------------------------
/docs/examples/mortgage/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: mortgage
7 |
71 |
72 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/docs/examples/pizza/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: pizza
7 |
8 |
14 |
15 |
16 |
17 |
receipt
18 |
19 |
20 |
Pizza price
21 |
25 |
26 |
27 |
28 |
Tax percent
29 |
33 |
34 |
35 |
36 |
Tip percent
37 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
56 |
57 |
61 |
62 |
63 |
64 |
68 |
69 |
70 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/docs/examples/pizza/styles.css:
--------------------------------------------------------------------------------
1 | /*! TACHYONS v4.11.1 | http://tachyons.io */
2 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}.border-box,a,article,aside,blockquote,body,code,dd,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,html,input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],legend,li,main,nav,ol,p,pre,section,table,td,textarea,th,tr,ul{box-sizing:border-box}.aspect-ratio{height:0;position:relative}.aspect-ratio--16x9{padding-bottom:56.25%}.aspect-ratio--9x16{padding-bottom:177.77%}.aspect-ratio--4x3{padding-bottom:75%}.aspect-ratio--3x4{padding-bottom:133.33%}.aspect-ratio--6x4{padding-bottom:66.6%}.aspect-ratio--4x6{padding-bottom:150%}.aspect-ratio--8x5{padding-bottom:62.5%}.aspect-ratio--5x8{padding-bottom:160%}.aspect-ratio--7x5{padding-bottom:71.42%}.aspect-ratio--5x7{padding-bottom:140%}.aspect-ratio--1x1{padding-bottom:100%}.aspect-ratio--object{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}img{max-width:100%}.cover{background-size:cover!important}.contain{background-size:contain!important}.bg-center{background-position:50%}.bg-center,.bg-top{background-repeat:no-repeat}.bg-top{background-position:top}.bg-right{background-position:100%}.bg-bottom,.bg-right{background-repeat:no-repeat}.bg-bottom{background-position:bottom}.bg-left{background-repeat:no-repeat;background-position:0}.outline{outline:1px solid}.outline-transparent{outline:1px solid transparent}.outline-0{outline:0}.ba{border-style:solid;border-width:1px}.bt{border-top-style:solid;border-top-width:1px}.br{border-right-style:solid;border-right-width:1px}.bb{border-bottom-style:solid;border-bottom-width:1px}.bl{border-left-style:solid;border-left-width:1px}.bn{border-style:none;border-width:0}.b--black{border-color:#000}.b--near-black{border-color:#111}.b--dark-gray{border-color:#333}.b--mid-gray{border-color:#555}.b--gray{border-color:#777}.b--silver{border-color:#999}.b--light-silver{border-color:#aaa}.b--moon-gray{border-color:#ccc}.b--light-gray{border-color:#eee}.b--near-white{border-color:#f4f4f4}.b--white{border-color:#fff}.b--white-90{border-color:hsla(0,0%,100%,.9)}.b--white-80{border-color:hsla(0,0%,100%,.8)}.b--white-70{border-color:hsla(0,0%,100%,.7)}.b--white-60{border-color:hsla(0,0%,100%,.6)}.b--white-50{border-color:hsla(0,0%,100%,.5)}.b--white-40{border-color:hsla(0,0%,100%,.4)}.b--white-30{border-color:hsla(0,0%,100%,.3)}.b--white-20{border-color:hsla(0,0%,100%,.2)}.b--white-10{border-color:hsla(0,0%,100%,.1)}.b--white-05{border-color:hsla(0,0%,100%,.05)}.b--white-025{border-color:hsla(0,0%,100%,.025)}.b--white-0125{border-color:hsla(0,0%,100%,.0125)}.b--black-90{border-color:rgba(0,0,0,.9)}.b--black-80{border-color:rgba(0,0,0,.8)}.b--black-70{border-color:rgba(0,0,0,.7)}.b--black-60{border-color:rgba(0,0,0,.6)}.b--black-50{border-color:rgba(0,0,0,.5)}.b--black-40{border-color:rgba(0,0,0,.4)}.b--black-30{border-color:rgba(0,0,0,.3)}.b--black-20{border-color:rgba(0,0,0,.2)}.b--black-10{border-color:rgba(0,0,0,.1)}.b--black-05{border-color:rgba(0,0,0,.05)}.b--black-025{border-color:rgba(0,0,0,.025)}.b--black-0125{border-color:rgba(0,0,0,.0125)}.b--dark-red{border-color:#e7040f}.b--red{border-color:#ff4136}.b--light-red{border-color:#ff725c}.b--orange{border-color:#ff6300}.b--gold{border-color:#ffb700}.b--yellow{border-color:gold}.b--light-yellow{border-color:#fbf1a9}.b--purple{border-color:#5e2ca5}.b--light-purple{border-color:#a463f2}.b--dark-pink{border-color:#d5008f}.b--hot-pink{border-color:#ff41b4}.b--pink{border-color:#ff80cc}.b--light-pink{border-color:#ffa3d7}.b--dark-green{border-color:#137752}.b--green{border-color:#19a974}.b--light-green{border-color:#9eebcf}.b--navy{border-color:#001b44}.b--dark-blue{border-color:#00449e}.b--blue{border-color:#357edd}.b--light-blue{border-color:#96ccff}.b--lightest-blue{border-color:#cdecff}.b--washed-blue{border-color:#f6fffe}.b--washed-green{border-color:#e8fdf5}.b--washed-yellow{border-color:#fffceb}.b--washed-red{border-color:#ffdfdf}.b--transparent{border-color:transparent}.b--inherit{border-color:inherit}.br0{border-radius:0}.br1{border-radius:.125rem}.br2{border-radius:.25rem}.br3{border-radius:.5rem}.br4{border-radius:1rem}.br-100{border-radius:100%}.br-pill{border-radius:9999px}.br--bottom{border-top-left-radius:0;border-top-right-radius:0}.br--top{border-bottom-right-radius:0}.br--right,.br--top{border-bottom-left-radius:0}.br--right{border-top-left-radius:0}.br--left{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted{border-style:dotted}.b--dashed{border-style:dashed}.b--solid{border-style:solid}.b--none{border-style:none}.bw0{border-width:0}.bw1{border-width:.125rem}.bw2{border-width:.25rem}.bw3{border-width:.5rem}.bw4{border-width:1rem}.bw5{border-width:2rem}.bt-0{border-top-width:0}.br-0{border-right-width:0}.bb-0{border-bottom-width:0}.bl-0{border-left-width:0}.shadow-1{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.pre{overflow-x:auto;overflow-y:hidden;overflow:scroll}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.top-1{top:1rem}.right-1{right:1rem}.bottom-1{bottom:1rem}.left-1{left:1rem}.top-2{top:2rem}.right-2{right:2rem}.bottom-2{bottom:2rem}.left-2{left:2rem}.top--1{top:-1rem}.right--1{right:-1rem}.bottom--1{bottom:-1rem}.left--1{left:-1rem}.top--2{top:-2rem}.right--2{right:-2rem}.bottom--2{bottom:-2rem}.left--2{left:-2rem}.absolute--fill{top:0;right:0;bottom:0;left:0}.cf:after,.cf:before{content:" ";display:table}.cf:after{clear:both}.cf{*zoom:1}.cl{clear:left}.cr{clear:right}.cb{clear:both}.cn{clear:none}.dn{display:none}.di{display:inline}.db{display:block}.dib{display:inline-block}.dit{display:inline-table}.dt{display:table}.dtc{display:table-cell}.dt-row{display:table-row}.dt-row-group{display:table-row-group}.dt-column{display:table-column}.dt-column-group{display:table-column-group}.dt--fixed{table-layout:fixed;width:100%}.flex{display:flex}.inline-flex{display:inline-flex}.flex-auto{flex:1 1 auto;min-width:0;min-height:0}.flex-none{flex:none}.flex-column{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.flex-column-reverse{flex-direction:column-reverse}.flex-row-reverse{flex-direction:row-reverse}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.self-baseline{align-self:baseline}.self-stretch{align-self:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.content-start{align-content:flex-start}.content-end{align-content:flex-end}.content-center{align-content:center}.content-between{align-content:space-between}.content-around{align-content:space-around}.content-stretch{align-content:stretch}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-last{order:99999}.flex-grow-0{flex-grow:0}.flex-grow-1{flex-grow:1}.flex-shrink-0{flex-shrink:0}.flex-shrink-1{flex-shrink:1}.fl{float:left}.fl,.fr{_display:inline}.fr{float:right}.fn{float:none}.sans-serif{font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.serif{font-family:georgia,times,serif}.system-sans-serif{font-family:sans-serif}.system-serif{font-family:serif}.code,code{font-family:Consolas,monaco,monospace}.courier{font-family:Courier Next,courier,monospace}.helvetica{font-family:helvetica neue,helvetica,sans-serif}.avenir{font-family:avenir next,avenir,sans-serif}.athelas{font-family:athelas,georgia,serif}.georgia{font-family:georgia,serif}.times{font-family:times,serif}.bodoni{font-family:Bodoni MT,serif}.calisto{font-family:Calisto MT,serif}.garamond{font-family:garamond,serif}.baskerville{font-family:baskerville,serif}.i{font-style:italic}.fs-normal{font-style:normal}.normal{font-weight:400}.b{font-weight:700}.fw1{font-weight:100}.fw2{font-weight:200}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fw9{font-weight:900}.input-reset{-webkit-appearance:none;-moz-appearance:none}.button-reset::-moz-focus-inner,.input-reset::-moz-focus-inner{border:0;padding:0}.h1{height:1rem}.h2{height:2rem}.h3{height:4rem}.h4{height:8rem}.h5{height:16rem}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.min-h-100{min-height:100%}.vh-25{height:25vh}.vh-50{height:50vh}.vh-75{height:75vh}.vh-100{height:100vh}.min-vh-100{min-height:100vh}.h-auto{height:auto}.h-inherit{height:inherit}.tracked{letter-spacing:.1em}.tracked-tight{letter-spacing:-.05em}.tracked-mega{letter-spacing:.25em}.lh-solid{line-height:1}.lh-title{line-height:1.25}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.list{list-style-type:none}.mw-100{max-width:100%}.mw1{max-width:1rem}.mw2{max-width:2rem}.mw3{max-width:4rem}.mw4{max-width:8rem}.mw5{max-width:16rem}.mw6{max-width:32rem}.mw7{max-width:48rem}.mw8{max-width:64rem}.mw9{max-width:96rem}.mw-none{max-width:none}.w1{width:1rem}.w2{width:2rem}.w3{width:4rem}.w4{width:8rem}.w5{width:16rem}.w-10{width:10%}.w-20{width:20%}.w-25{width:25%}.w-30{width:30%}.w-33{width:33%}.w-34{width:34%}.w-40{width:40%}.w-50{width:50%}.w-60{width:60%}.w-70{width:70%}.w-75{width:75%}.w-80{width:80%}.w-90{width:90%}.w-100{width:100%}.w-third{width:33.33333%}.w-two-thirds{width:66.66667%}.w-auto{width:auto}.overflow-visible{overflow:visible}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-auto{overflow:auto}.overflow-x-visible{overflow-x:visible}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-visible{overflow-y:visible}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-auto{overflow-y:auto}.static{position:static}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.o-100{opacity:1}.o-90{opacity:.9}.o-80{opacity:.8}.o-70{opacity:.7}.o-60{opacity:.6}.o-50{opacity:.5}.o-40{opacity:.4}.o-30{opacity:.3}.o-20{opacity:.2}.o-10{opacity:.1}.o-05{opacity:.05}.o-025{opacity:.025}.o-0{opacity:0}.rotate-45{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.black-90{color:rgba(0,0,0,.9)}.black-80{color:rgba(0,0,0,.8)}.black-70{color:rgba(0,0,0,.7)}.black-60{color:rgba(0,0,0,.6)}.black-50{color:rgba(0,0,0,.5)}.black-40{color:rgba(0,0,0,.4)}.black-30{color:rgba(0,0,0,.3)}.black-20{color:rgba(0,0,0,.2)}.black-10{color:rgba(0,0,0,.1)}.black-05{color:rgba(0,0,0,.05)}.white-90{color:hsla(0,0%,100%,.9)}.white-80{color:hsla(0,0%,100%,.8)}.white-70{color:hsla(0,0%,100%,.7)}.white-60{color:hsla(0,0%,100%,.6)}.white-50{color:hsla(0,0%,100%,.5)}.white-40{color:hsla(0,0%,100%,.4)}.white-30{color:hsla(0,0%,100%,.3)}.white-20{color:hsla(0,0%,100%,.2)}.white-10{color:hsla(0,0%,100%,.1)}.black{color:#000}.near-black{color:#111}.dark-gray{color:#333}.mid-gray{color:#555}.gray{color:#777}.silver{color:#999}.light-silver{color:#aaa}.moon-gray{color:#ccc}.light-gray{color:#eee}.near-white{color:#f4f4f4}.white{color:#fff}.dark-red{color:#e7040f}.red{color:#ff4136}.light-red{color:#ff725c}.orange{color:#ff6300}.gold{color:#ffb700}.yellow{color:gold}.light-yellow{color:#fbf1a9}.purple{color:#5e2ca5}.light-purple{color:#a463f2}.dark-pink{color:#d5008f}.hot-pink{color:#ff41b4}.pink{color:#ff80cc}.light-pink{color:#ffa3d7}.dark-green{color:#137752}.green{color:#19a974}.light-green{color:#9eebcf}.navy{color:#001b44}.dark-blue{color:#00449e}.blue{color:#357edd}.light-blue{color:#96ccff}.lightest-blue{color:#cdecff}.washed-blue{color:#f6fffe}.washed-green{color:#e8fdf5}.washed-yellow{color:#fffceb}.washed-red{color:#ffdfdf}.color-inherit{color:inherit}.bg-black-90{background-color:rgba(0,0,0,.9)}.bg-black-80{background-color:rgba(0,0,0,.8)}.bg-black-70{background-color:rgba(0,0,0,.7)}.bg-black-60{background-color:rgba(0,0,0,.6)}.bg-black-50{background-color:rgba(0,0,0,.5)}.bg-black-40{background-color:rgba(0,0,0,.4)}.bg-black-30{background-color:rgba(0,0,0,.3)}.bg-black-20{background-color:rgba(0,0,0,.2)}.bg-black-10{background-color:rgba(0,0,0,.1)}.bg-black-05{background-color:rgba(0,0,0,.05)}.bg-white-90{background-color:hsla(0,0%,100%,.9)}.bg-white-80{background-color:hsla(0,0%,100%,.8)}.bg-white-70{background-color:hsla(0,0%,100%,.7)}.bg-white-60{background-color:hsla(0,0%,100%,.6)}.bg-white-50{background-color:hsla(0,0%,100%,.5)}.bg-white-40{background-color:hsla(0,0%,100%,.4)}.bg-white-30{background-color:hsla(0,0%,100%,.3)}.bg-white-20{background-color:hsla(0,0%,100%,.2)}.bg-white-10{background-color:hsla(0,0%,100%,.1)}.bg-black{background-color:#000}.bg-near-black{background-color:#111}.bg-dark-gray{background-color:#333}.bg-mid-gray{background-color:#555}.bg-gray{background-color:#777}.bg-silver{background-color:#999}.bg-light-silver{background-color:#aaa}.bg-moon-gray{background-color:#ccc}.bg-light-gray{background-color:#eee}.bg-near-white{background-color:#f4f4f4}.bg-white{background-color:#fff}.bg-transparent{background-color:transparent}.bg-dark-red{background-color:#e7040f}.bg-red{background-color:#ff4136}.bg-light-red{background-color:#ff725c}.bg-orange{background-color:#ff6300}.bg-gold{background-color:#ffb700}.bg-yellow{background-color:gold}.bg-light-yellow{background-color:#fbf1a9}.bg-purple{background-color:#5e2ca5}.bg-light-purple{background-color:#a463f2}.bg-dark-pink{background-color:#d5008f}.bg-hot-pink{background-color:#ff41b4}.bg-pink{background-color:#ff80cc}.bg-light-pink{background-color:#ffa3d7}.bg-dark-green{background-color:#137752}.bg-green{background-color:#19a974}.bg-light-green{background-color:#9eebcf}.bg-navy{background-color:#001b44}.bg-dark-blue{background-color:#00449e}.bg-blue{background-color:#357edd}.bg-light-blue{background-color:#96ccff}.bg-lightest-blue{background-color:#cdecff}.bg-washed-blue{background-color:#f6fffe}.bg-washed-green{background-color:#e8fdf5}.bg-washed-yellow{background-color:#fffceb}.bg-washed-red{background-color:#ffdfdf}.bg-inherit{background-color:inherit}.hover-black:focus,.hover-black:hover{color:#000}.hover-near-black:focus,.hover-near-black:hover{color:#111}.hover-dark-gray:focus,.hover-dark-gray:hover{color:#333}.hover-mid-gray:focus,.hover-mid-gray:hover{color:#555}.hover-gray:focus,.hover-gray:hover{color:#777}.hover-silver:focus,.hover-silver:hover{color:#999}.hover-light-silver:focus,.hover-light-silver:hover{color:#aaa}.hover-moon-gray:focus,.hover-moon-gray:hover{color:#ccc}.hover-light-gray:focus,.hover-light-gray:hover{color:#eee}.hover-near-white:focus,.hover-near-white:hover{color:#f4f4f4}.hover-white:focus,.hover-white:hover{color:#fff}.hover-black-90:focus,.hover-black-90:hover{color:rgba(0,0,0,.9)}.hover-black-80:focus,.hover-black-80:hover{color:rgba(0,0,0,.8)}.hover-black-70:focus,.hover-black-70:hover{color:rgba(0,0,0,.7)}.hover-black-60:focus,.hover-black-60:hover{color:rgba(0,0,0,.6)}.hover-black-50:focus,.hover-black-50:hover{color:rgba(0,0,0,.5)}.hover-black-40:focus,.hover-black-40:hover{color:rgba(0,0,0,.4)}.hover-black-30:focus,.hover-black-30:hover{color:rgba(0,0,0,.3)}.hover-black-20:focus,.hover-black-20:hover{color:rgba(0,0,0,.2)}.hover-black-10:focus,.hover-black-10:hover{color:rgba(0,0,0,.1)}.hover-white-90:focus,.hover-white-90:hover{color:hsla(0,0%,100%,.9)}.hover-white-80:focus,.hover-white-80:hover{color:hsla(0,0%,100%,.8)}.hover-white-70:focus,.hover-white-70:hover{color:hsla(0,0%,100%,.7)}.hover-white-60:focus,.hover-white-60:hover{color:hsla(0,0%,100%,.6)}.hover-white-50:focus,.hover-white-50:hover{color:hsla(0,0%,100%,.5)}.hover-white-40:focus,.hover-white-40:hover{color:hsla(0,0%,100%,.4)}.hover-white-30:focus,.hover-white-30:hover{color:hsla(0,0%,100%,.3)}.hover-white-20:focus,.hover-white-20:hover{color:hsla(0,0%,100%,.2)}.hover-white-10:focus,.hover-white-10:hover{color:hsla(0,0%,100%,.1)}.hover-inherit:focus,.hover-inherit:hover{color:inherit}.hover-bg-black:focus,.hover-bg-black:hover{background-color:#000}.hover-bg-near-black:focus,.hover-bg-near-black:hover{background-color:#111}.hover-bg-dark-gray:focus,.hover-bg-dark-gray:hover{background-color:#333}.hover-bg-mid-gray:focus,.hover-bg-mid-gray:hover{background-color:#555}.hover-bg-gray:focus,.hover-bg-gray:hover{background-color:#777}.hover-bg-silver:focus,.hover-bg-silver:hover{background-color:#999}.hover-bg-light-silver:focus,.hover-bg-light-silver:hover{background-color:#aaa}.hover-bg-moon-gray:focus,.hover-bg-moon-gray:hover{background-color:#ccc}.hover-bg-light-gray:focus,.hover-bg-light-gray:hover{background-color:#eee}.hover-bg-near-white:focus,.hover-bg-near-white:hover{background-color:#f4f4f4}.hover-bg-white:focus,.hover-bg-white:hover{background-color:#fff}.hover-bg-transparent:focus,.hover-bg-transparent:hover{background-color:transparent}.hover-bg-black-90:focus,.hover-bg-black-90:hover{background-color:rgba(0,0,0,.9)}.hover-bg-black-80:focus,.hover-bg-black-80:hover{background-color:rgba(0,0,0,.8)}.hover-bg-black-70:focus,.hover-bg-black-70:hover{background-color:rgba(0,0,0,.7)}.hover-bg-black-60:focus,.hover-bg-black-60:hover{background-color:rgba(0,0,0,.6)}.hover-bg-black-50:focus,.hover-bg-black-50:hover{background-color:rgba(0,0,0,.5)}.hover-bg-black-40:focus,.hover-bg-black-40:hover{background-color:rgba(0,0,0,.4)}.hover-bg-black-30:focus,.hover-bg-black-30:hover{background-color:rgba(0,0,0,.3)}.hover-bg-black-20:focus,.hover-bg-black-20:hover{background-color:rgba(0,0,0,.2)}.hover-bg-black-10:focus,.hover-bg-black-10:hover{background-color:rgba(0,0,0,.1)}.hover-bg-white-90:focus,.hover-bg-white-90:hover{background-color:hsla(0,0%,100%,.9)}.hover-bg-white-80:focus,.hover-bg-white-80:hover{background-color:hsla(0,0%,100%,.8)}.hover-bg-white-70:focus,.hover-bg-white-70:hover{background-color:hsla(0,0%,100%,.7)}.hover-bg-white-60:focus,.hover-bg-white-60:hover{background-color:hsla(0,0%,100%,.6)}.hover-bg-white-50:focus,.hover-bg-white-50:hover{background-color:hsla(0,0%,100%,.5)}.hover-bg-white-40:focus,.hover-bg-white-40:hover{background-color:hsla(0,0%,100%,.4)}.hover-bg-white-30:focus,.hover-bg-white-30:hover{background-color:hsla(0,0%,100%,.3)}.hover-bg-white-20:focus,.hover-bg-white-20:hover{background-color:hsla(0,0%,100%,.2)}.hover-bg-white-10:focus,.hover-bg-white-10:hover{background-color:hsla(0,0%,100%,.1)}.hover-dark-red:focus,.hover-dark-red:hover{color:#e7040f}.hover-red:focus,.hover-red:hover{color:#ff4136}.hover-light-red:focus,.hover-light-red:hover{color:#ff725c}.hover-orange:focus,.hover-orange:hover{color:#ff6300}.hover-gold:focus,.hover-gold:hover{color:#ffb700}.hover-yellow:focus,.hover-yellow:hover{color:gold}.hover-light-yellow:focus,.hover-light-yellow:hover{color:#fbf1a9}.hover-purple:focus,.hover-purple:hover{color:#5e2ca5}.hover-light-purple:focus,.hover-light-purple:hover{color:#a463f2}.hover-dark-pink:focus,.hover-dark-pink:hover{color:#d5008f}.hover-hot-pink:focus,.hover-hot-pink:hover{color:#ff41b4}.hover-pink:focus,.hover-pink:hover{color:#ff80cc}.hover-light-pink:focus,.hover-light-pink:hover{color:#ffa3d7}.hover-dark-green:focus,.hover-dark-green:hover{color:#137752}.hover-green:focus,.hover-green:hover{color:#19a974}.hover-light-green:focus,.hover-light-green:hover{color:#9eebcf}.hover-navy:focus,.hover-navy:hover{color:#001b44}.hover-dark-blue:focus,.hover-dark-blue:hover{color:#00449e}.hover-blue:focus,.hover-blue:hover{color:#357edd}.hover-light-blue:focus,.hover-light-blue:hover{color:#96ccff}.hover-lightest-blue:focus,.hover-lightest-blue:hover{color:#cdecff}.hover-washed-blue:focus,.hover-washed-blue:hover{color:#f6fffe}.hover-washed-green:focus,.hover-washed-green:hover{color:#e8fdf5}.hover-washed-yellow:focus,.hover-washed-yellow:hover{color:#fffceb}.hover-washed-red:focus,.hover-washed-red:hover{color:#ffdfdf}.hover-bg-dark-red:focus,.hover-bg-dark-red:hover{background-color:#e7040f}.hover-bg-red:focus,.hover-bg-red:hover{background-color:#ff4136}.hover-bg-light-red:focus,.hover-bg-light-red:hover{background-color:#ff725c}.hover-bg-orange:focus,.hover-bg-orange:hover{background-color:#ff6300}.hover-bg-gold:focus,.hover-bg-gold:hover{background-color:#ffb700}.hover-bg-yellow:focus,.hover-bg-yellow:hover{background-color:gold}.hover-bg-light-yellow:focus,.hover-bg-light-yellow:hover{background-color:#fbf1a9}.hover-bg-purple:focus,.hover-bg-purple:hover{background-color:#5e2ca5}.hover-bg-light-purple:focus,.hover-bg-light-purple:hover{background-color:#a463f2}.hover-bg-dark-pink:focus,.hover-bg-dark-pink:hover{background-color:#d5008f}.hover-bg-hot-pink:focus,.hover-bg-hot-pink:hover{background-color:#ff41b4}.hover-bg-pink:focus,.hover-bg-pink:hover{background-color:#ff80cc}.hover-bg-light-pink:focus,.hover-bg-light-pink:hover{background-color:#ffa3d7}.hover-bg-dark-green:focus,.hover-bg-dark-green:hover{background-color:#137752}.hover-bg-green:focus,.hover-bg-green:hover{background-color:#19a974}.hover-bg-light-green:focus,.hover-bg-light-green:hover{background-color:#9eebcf}.hover-bg-navy:focus,.hover-bg-navy:hover{background-color:#001b44}.hover-bg-dark-blue:focus,.hover-bg-dark-blue:hover{background-color:#00449e}.hover-bg-blue:focus,.hover-bg-blue:hover{background-color:#357edd}.hover-bg-light-blue:focus,.hover-bg-light-blue:hover{background-color:#96ccff}.hover-bg-lightest-blue:focus,.hover-bg-lightest-blue:hover{background-color:#cdecff}.hover-bg-washed-blue:focus,.hover-bg-washed-blue:hover{background-color:#f6fffe}.hover-bg-washed-green:focus,.hover-bg-washed-green:hover{background-color:#e8fdf5}.hover-bg-washed-yellow:focus,.hover-bg-washed-yellow:hover{background-color:#fffceb}.hover-bg-washed-red:focus,.hover-bg-washed-red:hover{background-color:#ffdfdf}.hover-bg-inherit:focus,.hover-bg-inherit:hover{background-color:inherit}.pa0{padding:0}.pa1{padding:.25rem}.pa2{padding:.5rem}.pa3{padding:1rem}.pa4{padding:2rem}.pa5{padding:4rem}.pa6{padding:8rem}.pa7{padding:16rem}.pl0{padding-left:0}.pl1{padding-left:.25rem}.pl2{padding-left:.5rem}.pl3{padding-left:1rem}.pl4{padding-left:2rem}.pl5{padding-left:4rem}.pl6{padding-left:8rem}.pl7{padding-left:16rem}.pr0{padding-right:0}.pr1{padding-right:.25rem}.pr2{padding-right:.5rem}.pr3{padding-right:1rem}.pr4{padding-right:2rem}.pr5{padding-right:4rem}.pr6{padding-right:8rem}.pr7{padding-right:16rem}.pb0{padding-bottom:0}.pb1{padding-bottom:.25rem}.pb2{padding-bottom:.5rem}.pb3{padding-bottom:1rem}.pb4{padding-bottom:2rem}.pb5{padding-bottom:4rem}.pb6{padding-bottom:8rem}.pb7{padding-bottom:16rem}.pt0{padding-top:0}.pt1{padding-top:.25rem}.pt2{padding-top:.5rem}.pt3{padding-top:1rem}.pt4{padding-top:2rem}.pt5{padding-top:4rem}.pt6{padding-top:8rem}.pt7{padding-top:16rem}.pv0{padding-top:0;padding-bottom:0}.pv1{padding-top:.25rem;padding-bottom:.25rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.pv3{padding-top:1rem;padding-bottom:1rem}.pv4{padding-top:2rem;padding-bottom:2rem}.pv5{padding-top:4rem;padding-bottom:4rem}.pv6{padding-top:8rem;padding-bottom:8rem}.pv7{padding-top:16rem;padding-bottom:16rem}.ph0{padding-left:0;padding-right:0}.ph1{padding-left:.25rem;padding-right:.25rem}.ph2{padding-left:.5rem;padding-right:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ph5{padding-left:4rem;padding-right:4rem}.ph6{padding-left:8rem;padding-right:8rem}.ph7{padding-left:16rem;padding-right:16rem}.ma0{margin:0}.ma1{margin:.25rem}.ma2{margin:.5rem}.ma3{margin:1rem}.ma4{margin:2rem}.ma5{margin:4rem}.ma6{margin:8rem}.ma7{margin:16rem}.ml0{margin-left:0}.ml1{margin-left:.25rem}.ml2{margin-left:.5rem}.ml3{margin-left:1rem}.ml4{margin-left:2rem}.ml5{margin-left:4rem}.ml6{margin-left:8rem}.ml7{margin-left:16rem}.mr0{margin-right:0}.mr1{margin-right:.25rem}.mr2{margin-right:.5rem}.mr3{margin-right:1rem}.mr4{margin-right:2rem}.mr5{margin-right:4rem}.mr6{margin-right:8rem}.mr7{margin-right:16rem}.mb0{margin-bottom:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mb4{margin-bottom:2rem}.mb5{margin-bottom:4rem}.mb6{margin-bottom:8rem}.mb7{margin-bottom:16rem}.mt0{margin-top:0}.mt1{margin-top:.25rem}.mt2{margin-top:.5rem}.mt3{margin-top:1rem}.mt4{margin-top:2rem}.mt5{margin-top:4rem}.mt6{margin-top:8rem}.mt7{margin-top:16rem}.mv0{margin-top:0;margin-bottom:0}.mv1{margin-top:.25rem;margin-bottom:.25rem}.mv2{margin-top:.5rem;margin-bottom:.5rem}.mv3{margin-top:1rem;margin-bottom:1rem}.mv4{margin-top:2rem;margin-bottom:2rem}.mv5{margin-top:4rem;margin-bottom:4rem}.mv6{margin-top:8rem;margin-bottom:8rem}.mv7{margin-top:16rem;margin-bottom:16rem}.mh0{margin-left:0;margin-right:0}.mh1{margin-left:.25rem;margin-right:.25rem}.mh2{margin-left:.5rem;margin-right:.5rem}.mh3{margin-left:1rem;margin-right:1rem}.mh4{margin-left:2rem;margin-right:2rem}.mh5{margin-left:4rem;margin-right:4rem}.mh6{margin-left:8rem;margin-right:8rem}.mh7{margin-left:16rem;margin-right:16rem}.na1{margin:-.25rem}.na2{margin:-.5rem}.na3{margin:-1rem}.na4{margin:-2rem}.na5{margin:-4rem}.na6{margin:-8rem}.na7{margin:-16rem}.nl1{margin-left:-.25rem}.nl2{margin-left:-.5rem}.nl3{margin-left:-1rem}.nl4{margin-left:-2rem}.nl5{margin-left:-4rem}.nl6{margin-left:-8rem}.nl7{margin-left:-16rem}.nr1{margin-right:-.25rem}.nr2{margin-right:-.5rem}.nr3{margin-right:-1rem}.nr4{margin-right:-2rem}.nr5{margin-right:-4rem}.nr6{margin-right:-8rem}.nr7{margin-right:-16rem}.nb1{margin-bottom:-.25rem}.nb2{margin-bottom:-.5rem}.nb3{margin-bottom:-1rem}.nb4{margin-bottom:-2rem}.nb5{margin-bottom:-4rem}.nb6{margin-bottom:-8rem}.nb7{margin-bottom:-16rem}.nt1{margin-top:-.25rem}.nt2{margin-top:-.5rem}.nt3{margin-top:-1rem}.nt4{margin-top:-2rem}.nt5{margin-top:-4rem}.nt6{margin-top:-8rem}.nt7{margin-top:-16rem}.collapse{border-collapse:collapse;border-spacing:0}.striped--light-silver:nth-child(odd){background-color:#aaa}.striped--moon-gray:nth-child(odd){background-color:#ccc}.striped--light-gray:nth-child(odd){background-color:#eee}.striped--near-white:nth-child(odd){background-color:#f4f4f4}.stripe-light:nth-child(odd){background-color:hsla(0,0%,100%,.1)}.stripe-dark:nth-child(odd){background-color:rgba(0,0,0,.1)}.strike{text-decoration:line-through}.underline{text-decoration:underline}.no-underline{text-decoration:none}.tl{text-align:left}.tr{text-align:right}.tc{text-align:center}.tj{text-align:justify}.ttc{text-transform:capitalize}.ttl{text-transform:lowercase}.ttu{text-transform:uppercase}.ttn{text-transform:none}.f-6,.f-headline{font-size:6rem}.f-5,.f-subheadline{font-size:5rem}.f1{font-size:3rem}.f2{font-size:2.25rem}.f3{font-size:1.5rem}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.measure-wide{max-width:34em}.measure-narrow{max-width:20em}.indent{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps{font-variant:small-caps}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.overflow-container{overflow-y:scroll}.center{margin-left:auto}.center,.mr-auto{margin-right:auto}.ml-auto{margin-left:auto}.clip{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal{white-space:normal}.nowrap{white-space:nowrap}.pre{white-space:pre}.v-base{vertical-align:baseline}.v-mid{vertical-align:middle}.v-top{vertical-align:top}.v-btm{vertical-align:bottom}.dim{opacity:1}.dim,.dim:focus,.dim:hover{transition:opacity .15s ease-in}.dim:focus,.dim:hover{opacity:.5}.dim:active{opacity:.8;transition:opacity .15s ease-out}.glow,.glow:focus,.glow:hover{transition:opacity .15s ease-in}.glow:focus,.glow:hover{opacity:1}.hide-child .child{opacity:0;transition:opacity .15s ease-in}.hide-child:active .child,.hide-child:focus .child,.hide-child:hover .child{opacity:1;transition:opacity .15s ease-in}.underline-hover:focus,.underline-hover:hover{text-decoration:underline}.grow{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-out;transition:transform .25s ease-out;transition:transform .25s ease-out,-webkit-transform .25s ease-out}.grow:focus,.grow:hover{-webkit-transform:scale(1.05);transform:scale(1.05)}.grow:active{-webkit-transform:scale(.9);transform:scale(.9)}.grow-large{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-in-out;transition:transform .25s ease-in-out;transition:transform .25s ease-in-out,-webkit-transform .25s ease-in-out}.grow-large:focus,.grow-large:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.grow-large:active{-webkit-transform:scale(.95);transform:scale(.95)}.pointer:hover,.shadow-hover{cursor:pointer}.shadow-hover{position:relative;transition:all .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:after{content:"";box-shadow:0 0 16px 2px rgba(0,0,0,.2);border-radius:inherit;opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:-1;transition:opacity .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:focus:after,.shadow-hover:hover:after{opacity:1}.bg-animate,.bg-animate:focus,.bg-animate:hover{transition:background-color .15s ease-in-out}.z-0{z-index:0}.z-1{z-index:1}.z-2{z-index:2}.z-3{z-index:3}.z-4{z-index:4}.z-5{z-index:5}.z-999{z-index:999}.z-9999{z-index:9999}.z-max{z-index:2147483647}.z-inherit{z-index:inherit}.z-initial{z-index:auto}.z-unset{z-index:unset}.nested-copy-line-height ol,.nested-copy-line-height p,.nested-copy-line-height ul{line-height:1.5}.nested-headline-line-height h1,.nested-headline-line-height h2,.nested-headline-line-height h3,.nested-headline-line-height h4,.nested-headline-line-height h5,.nested-headline-line-height h6{line-height:1.25}.nested-list-reset ol,.nested-list-reset ul{padding-left:0;margin-left:0;list-style-type:none}.nested-copy-indent p+p{text-indent:1em;margin-top:0;margin-bottom:0}.nested-copy-separator p+p{margin-top:1.5em}.nested-img img{width:100%;max-width:100%;display:block}.nested-links a{color:#357edd;transition:color .15s ease-in}.nested-links a:focus,.nested-links a:hover{color:#96ccff;transition:color .15s ease-in}.debug *{outline:1px solid gold}.debug-white *{outline:1px solid #fff}.debug-black *{outline:1px solid #000}.debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFElEQVR4AWPAC97/9x0eCsAEPgwAVLshdpENIxcAAAAASUVORK5CYII=) repeat 0 0}.debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMklEQVR4AWOgCLz/b0epAa6UGuBOqQHOQHLUgFEDnAbcBZ4UGwDOkiCnkIhdgNgNxAYAiYlD+8sEuo8AAAAASUVORK5CYII=) repeat 0 0}.debug-grid-8-solid{background:#fff url(data:image/gif;base64,R0lGODdhCAAIAPEAAADw/wDx/////wAAACwAAAAACAAIAAACDZQvgaeb/lxbAIKA8y0AOw==) repeat 0 0}.debug-grid-16-solid{background:#fff url(data:image/gif;base64,R0lGODdhEAAQAPEAAADw/wDx/xXy/////ywAAAAAEAAQAAACIZyPKckYDQFsb6ZqD85jZ2+BkwiRFKehhqQCQgDHcgwEBQA7) repeat 0 0}@media screen and (min-width:30em){.aspect-ratio-ns{height:0;position:relative}.aspect-ratio--16x9-ns{padding-bottom:56.25%}.aspect-ratio--9x16-ns{padding-bottom:177.77%}.aspect-ratio--4x3-ns{padding-bottom:75%}.aspect-ratio--3x4-ns{padding-bottom:133.33%}.aspect-ratio--6x4-ns{padding-bottom:66.6%}.aspect-ratio--4x6-ns{padding-bottom:150%}.aspect-ratio--8x5-ns{padding-bottom:62.5%}.aspect-ratio--5x8-ns{padding-bottom:160%}.aspect-ratio--7x5-ns{padding-bottom:71.42%}.aspect-ratio--5x7-ns{padding-bottom:140%}.aspect-ratio--1x1-ns{padding-bottom:100%}.aspect-ratio--object-ns{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-ns{background-size:cover!important}.contain-ns{background-size:contain!important}.bg-center-ns{background-position:50%}.bg-center-ns,.bg-top-ns{background-repeat:no-repeat}.bg-top-ns{background-position:top}.bg-right-ns{background-position:100%}.bg-bottom-ns,.bg-right-ns{background-repeat:no-repeat}.bg-bottom-ns{background-position:bottom}.bg-left-ns{background-repeat:no-repeat;background-position:0}.outline-ns{outline:1px solid}.outline-transparent-ns{outline:1px solid transparent}.outline-0-ns{outline:0}.ba-ns{border-style:solid;border-width:1px}.bt-ns{border-top-style:solid;border-top-width:1px}.br-ns{border-right-style:solid;border-right-width:1px}.bb-ns{border-bottom-style:solid;border-bottom-width:1px}.bl-ns{border-left-style:solid;border-left-width:1px}.bn-ns{border-style:none;border-width:0}.br0-ns{border-radius:0}.br1-ns{border-radius:.125rem}.br2-ns{border-radius:.25rem}.br3-ns{border-radius:.5rem}.br4-ns{border-radius:1rem}.br-100-ns{border-radius:100%}.br-pill-ns{border-radius:9999px}.br--bottom-ns{border-top-left-radius:0;border-top-right-radius:0}.br--top-ns{border-bottom-right-radius:0}.br--right-ns,.br--top-ns{border-bottom-left-radius:0}.br--right-ns{border-top-left-radius:0}.br--left-ns{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-ns{border-style:dotted}.b--dashed-ns{border-style:dashed}.b--solid-ns{border-style:solid}.b--none-ns{border-style:none}.bw0-ns{border-width:0}.bw1-ns{border-width:.125rem}.bw2-ns{border-width:.25rem}.bw3-ns{border-width:.5rem}.bw4-ns{border-width:1rem}.bw5-ns{border-width:2rem}.bt-0-ns{border-top-width:0}.br-0-ns{border-right-width:0}.bb-0-ns{border-bottom-width:0}.bl-0-ns{border-left-width:0}.shadow-1-ns{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-ns{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-ns{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-ns{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-ns{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-ns{top:0}.left-0-ns{left:0}.right-0-ns{right:0}.bottom-0-ns{bottom:0}.top-1-ns{top:1rem}.left-1-ns{left:1rem}.right-1-ns{right:1rem}.bottom-1-ns{bottom:1rem}.top-2-ns{top:2rem}.left-2-ns{left:2rem}.right-2-ns{right:2rem}.bottom-2-ns{bottom:2rem}.top--1-ns{top:-1rem}.right--1-ns{right:-1rem}.bottom--1-ns{bottom:-1rem}.left--1-ns{left:-1rem}.top--2-ns{top:-2rem}.right--2-ns{right:-2rem}.bottom--2-ns{bottom:-2rem}.left--2-ns{left:-2rem}.absolute--fill-ns{top:0;right:0;bottom:0;left:0}.cl-ns{clear:left}.cr-ns{clear:right}.cb-ns{clear:both}.cn-ns{clear:none}.dn-ns{display:none}.di-ns{display:inline}.db-ns{display:block}.dib-ns{display:inline-block}.dit-ns{display:inline-table}.dt-ns{display:table}.dtc-ns{display:table-cell}.dt-row-ns{display:table-row}.dt-row-group-ns{display:table-row-group}.dt-column-ns{display:table-column}.dt-column-group-ns{display:table-column-group}.dt--fixed-ns{table-layout:fixed;width:100%}.flex-ns{display:flex}.inline-flex-ns{display:inline-flex}.flex-auto-ns{flex:1 1 auto;min-width:0;min-height:0}.flex-none-ns{flex:none}.flex-column-ns{flex-direction:column}.flex-row-ns{flex-direction:row}.flex-wrap-ns{flex-wrap:wrap}.flex-nowrap-ns{flex-wrap:nowrap}.flex-wrap-reverse-ns{flex-wrap:wrap-reverse}.flex-column-reverse-ns{flex-direction:column-reverse}.flex-row-reverse-ns{flex-direction:row-reverse}.items-start-ns{align-items:flex-start}.items-end-ns{align-items:flex-end}.items-center-ns{align-items:center}.items-baseline-ns{align-items:baseline}.items-stretch-ns{align-items:stretch}.self-start-ns{align-self:flex-start}.self-end-ns{align-self:flex-end}.self-center-ns{align-self:center}.self-baseline-ns{align-self:baseline}.self-stretch-ns{align-self:stretch}.justify-start-ns{justify-content:flex-start}.justify-end-ns{justify-content:flex-end}.justify-center-ns{justify-content:center}.justify-between-ns{justify-content:space-between}.justify-around-ns{justify-content:space-around}.content-start-ns{align-content:flex-start}.content-end-ns{align-content:flex-end}.content-center-ns{align-content:center}.content-between-ns{align-content:space-between}.content-around-ns{align-content:space-around}.content-stretch-ns{align-content:stretch}.order-0-ns{order:0}.order-1-ns{order:1}.order-2-ns{order:2}.order-3-ns{order:3}.order-4-ns{order:4}.order-5-ns{order:5}.order-6-ns{order:6}.order-7-ns{order:7}.order-8-ns{order:8}.order-last-ns{order:99999}.flex-grow-0-ns{flex-grow:0}.flex-grow-1-ns{flex-grow:1}.flex-shrink-0-ns{flex-shrink:0}.flex-shrink-1-ns{flex-shrink:1}.fl-ns{float:left}.fl-ns,.fr-ns{_display:inline}.fr-ns{float:right}.fn-ns{float:none}.i-ns{font-style:italic}.fs-normal-ns{font-style:normal}.normal-ns{font-weight:400}.b-ns{font-weight:700}.fw1-ns{font-weight:100}.fw2-ns{font-weight:200}.fw3-ns{font-weight:300}.fw4-ns{font-weight:400}.fw5-ns{font-weight:500}.fw6-ns{font-weight:600}.fw7-ns{font-weight:700}.fw8-ns{font-weight:800}.fw9-ns{font-weight:900}.h1-ns{height:1rem}.h2-ns{height:2rem}.h3-ns{height:4rem}.h4-ns{height:8rem}.h5-ns{height:16rem}.h-25-ns{height:25%}.h-50-ns{height:50%}.h-75-ns{height:75%}.h-100-ns{height:100%}.min-h-100-ns{min-height:100%}.vh-25-ns{height:25vh}.vh-50-ns{height:50vh}.vh-75-ns{height:75vh}.vh-100-ns{height:100vh}.min-vh-100-ns{min-height:100vh}.h-auto-ns{height:auto}.h-inherit-ns{height:inherit}.tracked-ns{letter-spacing:.1em}.tracked-tight-ns{letter-spacing:-.05em}.tracked-mega-ns{letter-spacing:.25em}.lh-solid-ns{line-height:1}.lh-title-ns{line-height:1.25}.lh-copy-ns{line-height:1.5}.mw-100-ns{max-width:100%}.mw1-ns{max-width:1rem}.mw2-ns{max-width:2rem}.mw3-ns{max-width:4rem}.mw4-ns{max-width:8rem}.mw5-ns{max-width:16rem}.mw6-ns{max-width:32rem}.mw7-ns{max-width:48rem}.mw8-ns{max-width:64rem}.mw9-ns{max-width:96rem}.mw-none-ns{max-width:none}.w1-ns{width:1rem}.w2-ns{width:2rem}.w3-ns{width:4rem}.w4-ns{width:8rem}.w5-ns{width:16rem}.w-10-ns{width:10%}.w-20-ns{width:20%}.w-25-ns{width:25%}.w-30-ns{width:30%}.w-33-ns{width:33%}.w-34-ns{width:34%}.w-40-ns{width:40%}.w-50-ns{width:50%}.w-60-ns{width:60%}.w-70-ns{width:70%}.w-75-ns{width:75%}.w-80-ns{width:80%}.w-90-ns{width:90%}.w-100-ns{width:100%}.w-third-ns{width:33.33333%}.w-two-thirds-ns{width:66.66667%}.w-auto-ns{width:auto}.overflow-visible-ns{overflow:visible}.overflow-hidden-ns{overflow:hidden}.overflow-scroll-ns{overflow:scroll}.overflow-auto-ns{overflow:auto}.overflow-x-visible-ns{overflow-x:visible}.overflow-x-hidden-ns{overflow-x:hidden}.overflow-x-scroll-ns{overflow-x:scroll}.overflow-x-auto-ns{overflow-x:auto}.overflow-y-visible-ns{overflow-y:visible}.overflow-y-hidden-ns{overflow-y:hidden}.overflow-y-scroll-ns{overflow-y:scroll}.overflow-y-auto-ns{overflow-y:auto}.static-ns{position:static}.relative-ns{position:relative}.absolute-ns{position:absolute}.fixed-ns{position:fixed}.rotate-45-ns{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-ns{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-ns{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-ns{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-ns{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-ns{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-ns{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-ns{padding:0}.pa1-ns{padding:.25rem}.pa2-ns{padding:.5rem}.pa3-ns{padding:1rem}.pa4-ns{padding:2rem}.pa5-ns{padding:4rem}.pa6-ns{padding:8rem}.pa7-ns{padding:16rem}.pl0-ns{padding-left:0}.pl1-ns{padding-left:.25rem}.pl2-ns{padding-left:.5rem}.pl3-ns{padding-left:1rem}.pl4-ns{padding-left:2rem}.pl5-ns{padding-left:4rem}.pl6-ns{padding-left:8rem}.pl7-ns{padding-left:16rem}.pr0-ns{padding-right:0}.pr1-ns{padding-right:.25rem}.pr2-ns{padding-right:.5rem}.pr3-ns{padding-right:1rem}.pr4-ns{padding-right:2rem}.pr5-ns{padding-right:4rem}.pr6-ns{padding-right:8rem}.pr7-ns{padding-right:16rem}.pb0-ns{padding-bottom:0}.pb1-ns{padding-bottom:.25rem}.pb2-ns{padding-bottom:.5rem}.pb3-ns{padding-bottom:1rem}.pb4-ns{padding-bottom:2rem}.pb5-ns{padding-bottom:4rem}.pb6-ns{padding-bottom:8rem}.pb7-ns{padding-bottom:16rem}.pt0-ns{padding-top:0}.pt1-ns{padding-top:.25rem}.pt2-ns{padding-top:.5rem}.pt3-ns{padding-top:1rem}.pt4-ns{padding-top:2rem}.pt5-ns{padding-top:4rem}.pt6-ns{padding-top:8rem}.pt7-ns{padding-top:16rem}.pv0-ns{padding-top:0;padding-bottom:0}.pv1-ns{padding-top:.25rem;padding-bottom:.25rem}.pv2-ns{padding-top:.5rem;padding-bottom:.5rem}.pv3-ns{padding-top:1rem;padding-bottom:1rem}.pv4-ns{padding-top:2rem;padding-bottom:2rem}.pv5-ns{padding-top:4rem;padding-bottom:4rem}.pv6-ns{padding-top:8rem;padding-bottom:8rem}.pv7-ns{padding-top:16rem;padding-bottom:16rem}.ph0-ns{padding-left:0;padding-right:0}.ph1-ns{padding-left:.25rem;padding-right:.25rem}.ph2-ns{padding-left:.5rem;padding-right:.5rem}.ph3-ns{padding-left:1rem;padding-right:1rem}.ph4-ns{padding-left:2rem;padding-right:2rem}.ph5-ns{padding-left:4rem;padding-right:4rem}.ph6-ns{padding-left:8rem;padding-right:8rem}.ph7-ns{padding-left:16rem;padding-right:16rem}.ma0-ns{margin:0}.ma1-ns{margin:.25rem}.ma2-ns{margin:.5rem}.ma3-ns{margin:1rem}.ma4-ns{margin:2rem}.ma5-ns{margin:4rem}.ma6-ns{margin:8rem}.ma7-ns{margin:16rem}.ml0-ns{margin-left:0}.ml1-ns{margin-left:.25rem}.ml2-ns{margin-left:.5rem}.ml3-ns{margin-left:1rem}.ml4-ns{margin-left:2rem}.ml5-ns{margin-left:4rem}.ml6-ns{margin-left:8rem}.ml7-ns{margin-left:16rem}.mr0-ns{margin-right:0}.mr1-ns{margin-right:.25rem}.mr2-ns{margin-right:.5rem}.mr3-ns{margin-right:1rem}.mr4-ns{margin-right:2rem}.mr5-ns{margin-right:4rem}.mr6-ns{margin-right:8rem}.mr7-ns{margin-right:16rem}.mb0-ns{margin-bottom:0}.mb1-ns{margin-bottom:.25rem}.mb2-ns{margin-bottom:.5rem}.mb3-ns{margin-bottom:1rem}.mb4-ns{margin-bottom:2rem}.mb5-ns{margin-bottom:4rem}.mb6-ns{margin-bottom:8rem}.mb7-ns{margin-bottom:16rem}.mt0-ns{margin-top:0}.mt1-ns{margin-top:.25rem}.mt2-ns{margin-top:.5rem}.mt3-ns{margin-top:1rem}.mt4-ns{margin-top:2rem}.mt5-ns{margin-top:4rem}.mt6-ns{margin-top:8rem}.mt7-ns{margin-top:16rem}.mv0-ns{margin-top:0;margin-bottom:0}.mv1-ns{margin-top:.25rem;margin-bottom:.25rem}.mv2-ns{margin-top:.5rem;margin-bottom:.5rem}.mv3-ns{margin-top:1rem;margin-bottom:1rem}.mv4-ns{margin-top:2rem;margin-bottom:2rem}.mv5-ns{margin-top:4rem;margin-bottom:4rem}.mv6-ns{margin-top:8rem;margin-bottom:8rem}.mv7-ns{margin-top:16rem;margin-bottom:16rem}.mh0-ns{margin-left:0;margin-right:0}.mh1-ns{margin-left:.25rem;margin-right:.25rem}.mh2-ns{margin-left:.5rem;margin-right:.5rem}.mh3-ns{margin-left:1rem;margin-right:1rem}.mh4-ns{margin-left:2rem;margin-right:2rem}.mh5-ns{margin-left:4rem;margin-right:4rem}.mh6-ns{margin-left:8rem;margin-right:8rem}.mh7-ns{margin-left:16rem;margin-right:16rem}.na1-ns{margin:-.25rem}.na2-ns{margin:-.5rem}.na3-ns{margin:-1rem}.na4-ns{margin:-2rem}.na5-ns{margin:-4rem}.na6-ns{margin:-8rem}.na7-ns{margin:-16rem}.nl1-ns{margin-left:-.25rem}.nl2-ns{margin-left:-.5rem}.nl3-ns{margin-left:-1rem}.nl4-ns{margin-left:-2rem}.nl5-ns{margin-left:-4rem}.nl6-ns{margin-left:-8rem}.nl7-ns{margin-left:-16rem}.nr1-ns{margin-right:-.25rem}.nr2-ns{margin-right:-.5rem}.nr3-ns{margin-right:-1rem}.nr4-ns{margin-right:-2rem}.nr5-ns{margin-right:-4rem}.nr6-ns{margin-right:-8rem}.nr7-ns{margin-right:-16rem}.nb1-ns{margin-bottom:-.25rem}.nb2-ns{margin-bottom:-.5rem}.nb3-ns{margin-bottom:-1rem}.nb4-ns{margin-bottom:-2rem}.nb5-ns{margin-bottom:-4rem}.nb6-ns{margin-bottom:-8rem}.nb7-ns{margin-bottom:-16rem}.nt1-ns{margin-top:-.25rem}.nt2-ns{margin-top:-.5rem}.nt3-ns{margin-top:-1rem}.nt4-ns{margin-top:-2rem}.nt5-ns{margin-top:-4rem}.nt6-ns{margin-top:-8rem}.nt7-ns{margin-top:-16rem}.strike-ns{text-decoration:line-through}.underline-ns{text-decoration:underline}.no-underline-ns{text-decoration:none}.tl-ns{text-align:left}.tr-ns{text-align:right}.tc-ns{text-align:center}.tj-ns{text-align:justify}.ttc-ns{text-transform:capitalize}.ttl-ns{text-transform:lowercase}.ttu-ns{text-transform:uppercase}.ttn-ns{text-transform:none}.f-6-ns,.f-headline-ns{font-size:6rem}.f-5-ns,.f-subheadline-ns{font-size:5rem}.f1-ns{font-size:3rem}.f2-ns{font-size:2.25rem}.f3-ns{font-size:1.5rem}.f4-ns{font-size:1.25rem}.f5-ns{font-size:1rem}.f6-ns{font-size:.875rem}.f7-ns{font-size:.75rem}.measure-ns{max-width:30em}.measure-wide-ns{max-width:34em}.measure-narrow-ns{max-width:20em}.indent-ns{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-ns{font-variant:small-caps}.truncate-ns{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-ns{margin-left:auto}.center-ns,.mr-auto-ns{margin-right:auto}.ml-auto-ns{margin-left:auto}.clip-ns{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-ns{white-space:normal}.nowrap-ns{white-space:nowrap}.pre-ns{white-space:pre}.v-base-ns{vertical-align:baseline}.v-mid-ns{vertical-align:middle}.v-top-ns{vertical-align:top}.v-btm-ns{vertical-align:bottom}}@media screen and (min-width:30em) and (max-width:60em){.aspect-ratio-m{height:0;position:relative}.aspect-ratio--16x9-m{padding-bottom:56.25%}.aspect-ratio--9x16-m{padding-bottom:177.77%}.aspect-ratio--4x3-m{padding-bottom:75%}.aspect-ratio--3x4-m{padding-bottom:133.33%}.aspect-ratio--6x4-m{padding-bottom:66.6%}.aspect-ratio--4x6-m{padding-bottom:150%}.aspect-ratio--8x5-m{padding-bottom:62.5%}.aspect-ratio--5x8-m{padding-bottom:160%}.aspect-ratio--7x5-m{padding-bottom:71.42%}.aspect-ratio--5x7-m{padding-bottom:140%}.aspect-ratio--1x1-m{padding-bottom:100%}.aspect-ratio--object-m{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-m{background-size:cover!important}.contain-m{background-size:contain!important}.bg-center-m{background-position:50%}.bg-center-m,.bg-top-m{background-repeat:no-repeat}.bg-top-m{background-position:top}.bg-right-m{background-position:100%}.bg-bottom-m,.bg-right-m{background-repeat:no-repeat}.bg-bottom-m{background-position:bottom}.bg-left-m{background-repeat:no-repeat;background-position:0}.outline-m{outline:1px solid}.outline-transparent-m{outline:1px solid transparent}.outline-0-m{outline:0}.ba-m{border-style:solid;border-width:1px}.bt-m{border-top-style:solid;border-top-width:1px}.br-m{border-right-style:solid;border-right-width:1px}.bb-m{border-bottom-style:solid;border-bottom-width:1px}.bl-m{border-left-style:solid;border-left-width:1px}.bn-m{border-style:none;border-width:0}.br0-m{border-radius:0}.br1-m{border-radius:.125rem}.br2-m{border-radius:.25rem}.br3-m{border-radius:.5rem}.br4-m{border-radius:1rem}.br-100-m{border-radius:100%}.br-pill-m{border-radius:9999px}.br--bottom-m{border-top-left-radius:0;border-top-right-radius:0}.br--top-m{border-bottom-right-radius:0}.br--right-m,.br--top-m{border-bottom-left-radius:0}.br--right-m{border-top-left-radius:0}.br--left-m{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-m{border-style:dotted}.b--dashed-m{border-style:dashed}.b--solid-m{border-style:solid}.b--none-m{border-style:none}.bw0-m{border-width:0}.bw1-m{border-width:.125rem}.bw2-m{border-width:.25rem}.bw3-m{border-width:.5rem}.bw4-m{border-width:1rem}.bw5-m{border-width:2rem}.bt-0-m{border-top-width:0}.br-0-m{border-right-width:0}.bb-0-m{border-bottom-width:0}.bl-0-m{border-left-width:0}.shadow-1-m{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-m{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-m{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-m{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-m{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-m{top:0}.left-0-m{left:0}.right-0-m{right:0}.bottom-0-m{bottom:0}.top-1-m{top:1rem}.left-1-m{left:1rem}.right-1-m{right:1rem}.bottom-1-m{bottom:1rem}.top-2-m{top:2rem}.left-2-m{left:2rem}.right-2-m{right:2rem}.bottom-2-m{bottom:2rem}.top--1-m{top:-1rem}.right--1-m{right:-1rem}.bottom--1-m{bottom:-1rem}.left--1-m{left:-1rem}.top--2-m{top:-2rem}.right--2-m{right:-2rem}.bottom--2-m{bottom:-2rem}.left--2-m{left:-2rem}.absolute--fill-m{top:0;right:0;bottom:0;left:0}.cl-m{clear:left}.cr-m{clear:right}.cb-m{clear:both}.cn-m{clear:none}.dn-m{display:none}.di-m{display:inline}.db-m{display:block}.dib-m{display:inline-block}.dit-m{display:inline-table}.dt-m{display:table}.dtc-m{display:table-cell}.dt-row-m{display:table-row}.dt-row-group-m{display:table-row-group}.dt-column-m{display:table-column}.dt-column-group-m{display:table-column-group}.dt--fixed-m{table-layout:fixed;width:100%}.flex-m{display:flex}.inline-flex-m{display:inline-flex}.flex-auto-m{flex:1 1 auto;min-width:0;min-height:0}.flex-none-m{flex:none}.flex-column-m{flex-direction:column}.flex-row-m{flex-direction:row}.flex-wrap-m{flex-wrap:wrap}.flex-nowrap-m{flex-wrap:nowrap}.flex-wrap-reverse-m{flex-wrap:wrap-reverse}.flex-column-reverse-m{flex-direction:column-reverse}.flex-row-reverse-m{flex-direction:row-reverse}.items-start-m{align-items:flex-start}.items-end-m{align-items:flex-end}.items-center-m{align-items:center}.items-baseline-m{align-items:baseline}.items-stretch-m{align-items:stretch}.self-start-m{align-self:flex-start}.self-end-m{align-self:flex-end}.self-center-m{align-self:center}.self-baseline-m{align-self:baseline}.self-stretch-m{align-self:stretch}.justify-start-m{justify-content:flex-start}.justify-end-m{justify-content:flex-end}.justify-center-m{justify-content:center}.justify-between-m{justify-content:space-between}.justify-around-m{justify-content:space-around}.content-start-m{align-content:flex-start}.content-end-m{align-content:flex-end}.content-center-m{align-content:center}.content-between-m{align-content:space-between}.content-around-m{align-content:space-around}.content-stretch-m{align-content:stretch}.order-0-m{order:0}.order-1-m{order:1}.order-2-m{order:2}.order-3-m{order:3}.order-4-m{order:4}.order-5-m{order:5}.order-6-m{order:6}.order-7-m{order:7}.order-8-m{order:8}.order-last-m{order:99999}.flex-grow-0-m{flex-grow:0}.flex-grow-1-m{flex-grow:1}.flex-shrink-0-m{flex-shrink:0}.flex-shrink-1-m{flex-shrink:1}.fl-m{float:left}.fl-m,.fr-m{_display:inline}.fr-m{float:right}.fn-m{float:none}.i-m{font-style:italic}.fs-normal-m{font-style:normal}.normal-m{font-weight:400}.b-m{font-weight:700}.fw1-m{font-weight:100}.fw2-m{font-weight:200}.fw3-m{font-weight:300}.fw4-m{font-weight:400}.fw5-m{font-weight:500}.fw6-m{font-weight:600}.fw7-m{font-weight:700}.fw8-m{font-weight:800}.fw9-m{font-weight:900}.h1-m{height:1rem}.h2-m{height:2rem}.h3-m{height:4rem}.h4-m{height:8rem}.h5-m{height:16rem}.h-25-m{height:25%}.h-50-m{height:50%}.h-75-m{height:75%}.h-100-m{height:100%}.min-h-100-m{min-height:100%}.vh-25-m{height:25vh}.vh-50-m{height:50vh}.vh-75-m{height:75vh}.vh-100-m{height:100vh}.min-vh-100-m{min-height:100vh}.h-auto-m{height:auto}.h-inherit-m{height:inherit}.tracked-m{letter-spacing:.1em}.tracked-tight-m{letter-spacing:-.05em}.tracked-mega-m{letter-spacing:.25em}.lh-solid-m{line-height:1}.lh-title-m{line-height:1.25}.lh-copy-m{line-height:1.5}.mw-100-m{max-width:100%}.mw1-m{max-width:1rem}.mw2-m{max-width:2rem}.mw3-m{max-width:4rem}.mw4-m{max-width:8rem}.mw5-m{max-width:16rem}.mw6-m{max-width:32rem}.mw7-m{max-width:48rem}.mw8-m{max-width:64rem}.mw9-m{max-width:96rem}.mw-none-m{max-width:none}.w1-m{width:1rem}.w2-m{width:2rem}.w3-m{width:4rem}.w4-m{width:8rem}.w5-m{width:16rem}.w-10-m{width:10%}.w-20-m{width:20%}.w-25-m{width:25%}.w-30-m{width:30%}.w-33-m{width:33%}.w-34-m{width:34%}.w-40-m{width:40%}.w-50-m{width:50%}.w-60-m{width:60%}.w-70-m{width:70%}.w-75-m{width:75%}.w-80-m{width:80%}.w-90-m{width:90%}.w-100-m{width:100%}.w-third-m{width:33.33333%}.w-two-thirds-m{width:66.66667%}.w-auto-m{width:auto}.overflow-visible-m{overflow:visible}.overflow-hidden-m{overflow:hidden}.overflow-scroll-m{overflow:scroll}.overflow-auto-m{overflow:auto}.overflow-x-visible-m{overflow-x:visible}.overflow-x-hidden-m{overflow-x:hidden}.overflow-x-scroll-m{overflow-x:scroll}.overflow-x-auto-m{overflow-x:auto}.overflow-y-visible-m{overflow-y:visible}.overflow-y-hidden-m{overflow-y:hidden}.overflow-y-scroll-m{overflow-y:scroll}.overflow-y-auto-m{overflow-y:auto}.static-m{position:static}.relative-m{position:relative}.absolute-m{position:absolute}.fixed-m{position:fixed}.rotate-45-m{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-m{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-m{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-m{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-m{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-m{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-m{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-m{padding:0}.pa1-m{padding:.25rem}.pa2-m{padding:.5rem}.pa3-m{padding:1rem}.pa4-m{padding:2rem}.pa5-m{padding:4rem}.pa6-m{padding:8rem}.pa7-m{padding:16rem}.pl0-m{padding-left:0}.pl1-m{padding-left:.25rem}.pl2-m{padding-left:.5rem}.pl3-m{padding-left:1rem}.pl4-m{padding-left:2rem}.pl5-m{padding-left:4rem}.pl6-m{padding-left:8rem}.pl7-m{padding-left:16rem}.pr0-m{padding-right:0}.pr1-m{padding-right:.25rem}.pr2-m{padding-right:.5rem}.pr3-m{padding-right:1rem}.pr4-m{padding-right:2rem}.pr5-m{padding-right:4rem}.pr6-m{padding-right:8rem}.pr7-m{padding-right:16rem}.pb0-m{padding-bottom:0}.pb1-m{padding-bottom:.25rem}.pb2-m{padding-bottom:.5rem}.pb3-m{padding-bottom:1rem}.pb4-m{padding-bottom:2rem}.pb5-m{padding-bottom:4rem}.pb6-m{padding-bottom:8rem}.pb7-m{padding-bottom:16rem}.pt0-m{padding-top:0}.pt1-m{padding-top:.25rem}.pt2-m{padding-top:.5rem}.pt3-m{padding-top:1rem}.pt4-m{padding-top:2rem}.pt5-m{padding-top:4rem}.pt6-m{padding-top:8rem}.pt7-m{padding-top:16rem}.pv0-m{padding-top:0;padding-bottom:0}.pv1-m{padding-top:.25rem;padding-bottom:.25rem}.pv2-m{padding-top:.5rem;padding-bottom:.5rem}.pv3-m{padding-top:1rem;padding-bottom:1rem}.pv4-m{padding-top:2rem;padding-bottom:2rem}.pv5-m{padding-top:4rem;padding-bottom:4rem}.pv6-m{padding-top:8rem;padding-bottom:8rem}.pv7-m{padding-top:16rem;padding-bottom:16rem}.ph0-m{padding-left:0;padding-right:0}.ph1-m{padding-left:.25rem;padding-right:.25rem}.ph2-m{padding-left:.5rem;padding-right:.5rem}.ph3-m{padding-left:1rem;padding-right:1rem}.ph4-m{padding-left:2rem;padding-right:2rem}.ph5-m{padding-left:4rem;padding-right:4rem}.ph6-m{padding-left:8rem;padding-right:8rem}.ph7-m{padding-left:16rem;padding-right:16rem}.ma0-m{margin:0}.ma1-m{margin:.25rem}.ma2-m{margin:.5rem}.ma3-m{margin:1rem}.ma4-m{margin:2rem}.ma5-m{margin:4rem}.ma6-m{margin:8rem}.ma7-m{margin:16rem}.ml0-m{margin-left:0}.ml1-m{margin-left:.25rem}.ml2-m{margin-left:.5rem}.ml3-m{margin-left:1rem}.ml4-m{margin-left:2rem}.ml5-m{margin-left:4rem}.ml6-m{margin-left:8rem}.ml7-m{margin-left:16rem}.mr0-m{margin-right:0}.mr1-m{margin-right:.25rem}.mr2-m{margin-right:.5rem}.mr3-m{margin-right:1rem}.mr4-m{margin-right:2rem}.mr5-m{margin-right:4rem}.mr6-m{margin-right:8rem}.mr7-m{margin-right:16rem}.mb0-m{margin-bottom:0}.mb1-m{margin-bottom:.25rem}.mb2-m{margin-bottom:.5rem}.mb3-m{margin-bottom:1rem}.mb4-m{margin-bottom:2rem}.mb5-m{margin-bottom:4rem}.mb6-m{margin-bottom:8rem}.mb7-m{margin-bottom:16rem}.mt0-m{margin-top:0}.mt1-m{margin-top:.25rem}.mt2-m{margin-top:.5rem}.mt3-m{margin-top:1rem}.mt4-m{margin-top:2rem}.mt5-m{margin-top:4rem}.mt6-m{margin-top:8rem}.mt7-m{margin-top:16rem}.mv0-m{margin-top:0;margin-bottom:0}.mv1-m{margin-top:.25rem;margin-bottom:.25rem}.mv2-m{margin-top:.5rem;margin-bottom:.5rem}.mv3-m{margin-top:1rem;margin-bottom:1rem}.mv4-m{margin-top:2rem;margin-bottom:2rem}.mv5-m{margin-top:4rem;margin-bottom:4rem}.mv6-m{margin-top:8rem;margin-bottom:8rem}.mv7-m{margin-top:16rem;margin-bottom:16rem}.mh0-m{margin-left:0;margin-right:0}.mh1-m{margin-left:.25rem;margin-right:.25rem}.mh2-m{margin-left:.5rem;margin-right:.5rem}.mh3-m{margin-left:1rem;margin-right:1rem}.mh4-m{margin-left:2rem;margin-right:2rem}.mh5-m{margin-left:4rem;margin-right:4rem}.mh6-m{margin-left:8rem;margin-right:8rem}.mh7-m{margin-left:16rem;margin-right:16rem}.na1-m{margin:-.25rem}.na2-m{margin:-.5rem}.na3-m{margin:-1rem}.na4-m{margin:-2rem}.na5-m{margin:-4rem}.na6-m{margin:-8rem}.na7-m{margin:-16rem}.nl1-m{margin-left:-.25rem}.nl2-m{margin-left:-.5rem}.nl3-m{margin-left:-1rem}.nl4-m{margin-left:-2rem}.nl5-m{margin-left:-4rem}.nl6-m{margin-left:-8rem}.nl7-m{margin-left:-16rem}.nr1-m{margin-right:-.25rem}.nr2-m{margin-right:-.5rem}.nr3-m{margin-right:-1rem}.nr4-m{margin-right:-2rem}.nr5-m{margin-right:-4rem}.nr6-m{margin-right:-8rem}.nr7-m{margin-right:-16rem}.nb1-m{margin-bottom:-.25rem}.nb2-m{margin-bottom:-.5rem}.nb3-m{margin-bottom:-1rem}.nb4-m{margin-bottom:-2rem}.nb5-m{margin-bottom:-4rem}.nb6-m{margin-bottom:-8rem}.nb7-m{margin-bottom:-16rem}.nt1-m{margin-top:-.25rem}.nt2-m{margin-top:-.5rem}.nt3-m{margin-top:-1rem}.nt4-m{margin-top:-2rem}.nt5-m{margin-top:-4rem}.nt6-m{margin-top:-8rem}.nt7-m{margin-top:-16rem}.strike-m{text-decoration:line-through}.underline-m{text-decoration:underline}.no-underline-m{text-decoration:none}.tl-m{text-align:left}.tr-m{text-align:right}.tc-m{text-align:center}.tj-m{text-align:justify}.ttc-m{text-transform:capitalize}.ttl-m{text-transform:lowercase}.ttu-m{text-transform:uppercase}.ttn-m{text-transform:none}.f-6-m,.f-headline-m{font-size:6rem}.f-5-m,.f-subheadline-m{font-size:5rem}.f1-m{font-size:3rem}.f2-m{font-size:2.25rem}.f3-m{font-size:1.5rem}.f4-m{font-size:1.25rem}.f5-m{font-size:1rem}.f6-m{font-size:.875rem}.f7-m{font-size:.75rem}.measure-m{max-width:30em}.measure-wide-m{max-width:34em}.measure-narrow-m{max-width:20em}.indent-m{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-m{font-variant:small-caps}.truncate-m{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-m{margin-left:auto}.center-m,.mr-auto-m{margin-right:auto}.ml-auto-m{margin-left:auto}.clip-m{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-m{white-space:normal}.nowrap-m{white-space:nowrap}.pre-m{white-space:pre}.v-base-m{vertical-align:baseline}.v-mid-m{vertical-align:middle}.v-top-m{vertical-align:top}.v-btm-m{vertical-align:bottom}}@media screen and (min-width:60em){.aspect-ratio-l{height:0;position:relative}.aspect-ratio--16x9-l{padding-bottom:56.25%}.aspect-ratio--9x16-l{padding-bottom:177.77%}.aspect-ratio--4x3-l{padding-bottom:75%}.aspect-ratio--3x4-l{padding-bottom:133.33%}.aspect-ratio--6x4-l{padding-bottom:66.6%}.aspect-ratio--4x6-l{padding-bottom:150%}.aspect-ratio--8x5-l{padding-bottom:62.5%}.aspect-ratio--5x8-l{padding-bottom:160%}.aspect-ratio--7x5-l{padding-bottom:71.42%}.aspect-ratio--5x7-l{padding-bottom:140%}.aspect-ratio--1x1-l{padding-bottom:100%}.aspect-ratio--object-l{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-l{background-size:cover!important}.contain-l{background-size:contain!important}.bg-center-l{background-position:50%}.bg-center-l,.bg-top-l{background-repeat:no-repeat}.bg-top-l{background-position:top}.bg-right-l{background-position:100%}.bg-bottom-l,.bg-right-l{background-repeat:no-repeat}.bg-bottom-l{background-position:bottom}.bg-left-l{background-repeat:no-repeat;background-position:0}.outline-l{outline:1px solid}.outline-transparent-l{outline:1px solid transparent}.outline-0-l{outline:0}.ba-l{border-style:solid;border-width:1px}.bt-l{border-top-style:solid;border-top-width:1px}.br-l{border-right-style:solid;border-right-width:1px}.bb-l{border-bottom-style:solid;border-bottom-width:1px}.bl-l{border-left-style:solid;border-left-width:1px}.bn-l{border-style:none;border-width:0}.br0-l{border-radius:0}.br1-l{border-radius:.125rem}.br2-l{border-radius:.25rem}.br3-l{border-radius:.5rem}.br4-l{border-radius:1rem}.br-100-l{border-radius:100%}.br-pill-l{border-radius:9999px}.br--bottom-l{border-top-left-radius:0;border-top-right-radius:0}.br--top-l{border-bottom-right-radius:0}.br--right-l,.br--top-l{border-bottom-left-radius:0}.br--right-l{border-top-left-radius:0}.br--left-l{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-l{border-style:dotted}.b--dashed-l{border-style:dashed}.b--solid-l{border-style:solid}.b--none-l{border-style:none}.bw0-l{border-width:0}.bw1-l{border-width:.125rem}.bw2-l{border-width:.25rem}.bw3-l{border-width:.5rem}.bw4-l{border-width:1rem}.bw5-l{border-width:2rem}.bt-0-l{border-top-width:0}.br-0-l{border-right-width:0}.bb-0-l{border-bottom-width:0}.bl-0-l{border-left-width:0}.shadow-1-l{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-l{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-l{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-l{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-l{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-l{top:0}.left-0-l{left:0}.right-0-l{right:0}.bottom-0-l{bottom:0}.top-1-l{top:1rem}.left-1-l{left:1rem}.right-1-l{right:1rem}.bottom-1-l{bottom:1rem}.top-2-l{top:2rem}.left-2-l{left:2rem}.right-2-l{right:2rem}.bottom-2-l{bottom:2rem}.top--1-l{top:-1rem}.right--1-l{right:-1rem}.bottom--1-l{bottom:-1rem}.left--1-l{left:-1rem}.top--2-l{top:-2rem}.right--2-l{right:-2rem}.bottom--2-l{bottom:-2rem}.left--2-l{left:-2rem}.absolute--fill-l{top:0;right:0;bottom:0;left:0}.cl-l{clear:left}.cr-l{clear:right}.cb-l{clear:both}.cn-l{clear:none}.dn-l{display:none}.di-l{display:inline}.db-l{display:block}.dib-l{display:inline-block}.dit-l{display:inline-table}.dt-l{display:table}.dtc-l{display:table-cell}.dt-row-l{display:table-row}.dt-row-group-l{display:table-row-group}.dt-column-l{display:table-column}.dt-column-group-l{display:table-column-group}.dt--fixed-l{table-layout:fixed;width:100%}.flex-l{display:flex}.inline-flex-l{display:inline-flex}.flex-auto-l{flex:1 1 auto;min-width:0;min-height:0}.flex-none-l{flex:none}.flex-column-l{flex-direction:column}.flex-row-l{flex-direction:row}.flex-wrap-l{flex-wrap:wrap}.flex-nowrap-l{flex-wrap:nowrap}.flex-wrap-reverse-l{flex-wrap:wrap-reverse}.flex-column-reverse-l{flex-direction:column-reverse}.flex-row-reverse-l{flex-direction:row-reverse}.items-start-l{align-items:flex-start}.items-end-l{align-items:flex-end}.items-center-l{align-items:center}.items-baseline-l{align-items:baseline}.items-stretch-l{align-items:stretch}.self-start-l{align-self:flex-start}.self-end-l{align-self:flex-end}.self-center-l{align-self:center}.self-baseline-l{align-self:baseline}.self-stretch-l{align-self:stretch}.justify-start-l{justify-content:flex-start}.justify-end-l{justify-content:flex-end}.justify-center-l{justify-content:center}.justify-between-l{justify-content:space-between}.justify-around-l{justify-content:space-around}.content-start-l{align-content:flex-start}.content-end-l{align-content:flex-end}.content-center-l{align-content:center}.content-between-l{align-content:space-between}.content-around-l{align-content:space-around}.content-stretch-l{align-content:stretch}.order-0-l{order:0}.order-1-l{order:1}.order-2-l{order:2}.order-3-l{order:3}.order-4-l{order:4}.order-5-l{order:5}.order-6-l{order:6}.order-7-l{order:7}.order-8-l{order:8}.order-last-l{order:99999}.flex-grow-0-l{flex-grow:0}.flex-grow-1-l{flex-grow:1}.flex-shrink-0-l{flex-shrink:0}.flex-shrink-1-l{flex-shrink:1}.fl-l{float:left}.fl-l,.fr-l{_display:inline}.fr-l{float:right}.fn-l{float:none}.i-l{font-style:italic}.fs-normal-l{font-style:normal}.normal-l{font-weight:400}.b-l{font-weight:700}.fw1-l{font-weight:100}.fw2-l{font-weight:200}.fw3-l{font-weight:300}.fw4-l{font-weight:400}.fw5-l{font-weight:500}.fw6-l{font-weight:600}.fw7-l{font-weight:700}.fw8-l{font-weight:800}.fw9-l{font-weight:900}.h1-l{height:1rem}.h2-l{height:2rem}.h3-l{height:4rem}.h4-l{height:8rem}.h5-l{height:16rem}.h-25-l{height:25%}.h-50-l{height:50%}.h-75-l{height:75%}.h-100-l{height:100%}.min-h-100-l{min-height:100%}.vh-25-l{height:25vh}.vh-50-l{height:50vh}.vh-75-l{height:75vh}.vh-100-l{height:100vh}.min-vh-100-l{min-height:100vh}.h-auto-l{height:auto}.h-inherit-l{height:inherit}.tracked-l{letter-spacing:.1em}.tracked-tight-l{letter-spacing:-.05em}.tracked-mega-l{letter-spacing:.25em}.lh-solid-l{line-height:1}.lh-title-l{line-height:1.25}.lh-copy-l{line-height:1.5}.mw-100-l{max-width:100%}.mw1-l{max-width:1rem}.mw2-l{max-width:2rem}.mw3-l{max-width:4rem}.mw4-l{max-width:8rem}.mw5-l{max-width:16rem}.mw6-l{max-width:32rem}.mw7-l{max-width:48rem}.mw8-l{max-width:64rem}.mw9-l{max-width:96rem}.mw-none-l{max-width:none}.w1-l{width:1rem}.w2-l{width:2rem}.w3-l{width:4rem}.w4-l{width:8rem}.w5-l{width:16rem}.w-10-l{width:10%}.w-20-l{width:20%}.w-25-l{width:25%}.w-30-l{width:30%}.w-33-l{width:33%}.w-34-l{width:34%}.w-40-l{width:40%}.w-50-l{width:50%}.w-60-l{width:60%}.w-70-l{width:70%}.w-75-l{width:75%}.w-80-l{width:80%}.w-90-l{width:90%}.w-100-l{width:100%}.w-third-l{width:33.33333%}.w-two-thirds-l{width:66.66667%}.w-auto-l{width:auto}.overflow-visible-l{overflow:visible}.overflow-hidden-l{overflow:hidden}.overflow-scroll-l{overflow:scroll}.overflow-auto-l{overflow:auto}.overflow-x-visible-l{overflow-x:visible}.overflow-x-hidden-l{overflow-x:hidden}.overflow-x-scroll-l{overflow-x:scroll}.overflow-x-auto-l{overflow-x:auto}.overflow-y-visible-l{overflow-y:visible}.overflow-y-hidden-l{overflow-y:hidden}.overflow-y-scroll-l{overflow-y:scroll}.overflow-y-auto-l{overflow-y:auto}.static-l{position:static}.relative-l{position:relative}.absolute-l{position:absolute}.fixed-l{position:fixed}.rotate-45-l{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-l{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-l{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-l{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-l{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-l{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-l{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-l{padding:0}.pa1-l{padding:.25rem}.pa2-l{padding:.5rem}.pa3-l{padding:1rem}.pa4-l{padding:2rem}.pa5-l{padding:4rem}.pa6-l{padding:8rem}.pa7-l{padding:16rem}.pl0-l{padding-left:0}.pl1-l{padding-left:.25rem}.pl2-l{padding-left:.5rem}.pl3-l{padding-left:1rem}.pl4-l{padding-left:2rem}.pl5-l{padding-left:4rem}.pl6-l{padding-left:8rem}.pl7-l{padding-left:16rem}.pr0-l{padding-right:0}.pr1-l{padding-right:.25rem}.pr2-l{padding-right:.5rem}.pr3-l{padding-right:1rem}.pr4-l{padding-right:2rem}.pr5-l{padding-right:4rem}.pr6-l{padding-right:8rem}.pr7-l{padding-right:16rem}.pb0-l{padding-bottom:0}.pb1-l{padding-bottom:.25rem}.pb2-l{padding-bottom:.5rem}.pb3-l{padding-bottom:1rem}.pb4-l{padding-bottom:2rem}.pb5-l{padding-bottom:4rem}.pb6-l{padding-bottom:8rem}.pb7-l{padding-bottom:16rem}.pt0-l{padding-top:0}.pt1-l{padding-top:.25rem}.pt2-l{padding-top:.5rem}.pt3-l{padding-top:1rem}.pt4-l{padding-top:2rem}.pt5-l{padding-top:4rem}.pt6-l{padding-top:8rem}.pt7-l{padding-top:16rem}.pv0-l{padding-top:0;padding-bottom:0}.pv1-l{padding-top:.25rem;padding-bottom:.25rem}.pv2-l{padding-top:.5rem;padding-bottom:.5rem}.pv3-l{padding-top:1rem;padding-bottom:1rem}.pv4-l{padding-top:2rem;padding-bottom:2rem}.pv5-l{padding-top:4rem;padding-bottom:4rem}.pv6-l{padding-top:8rem;padding-bottom:8rem}.pv7-l{padding-top:16rem;padding-bottom:16rem}.ph0-l{padding-left:0;padding-right:0}.ph1-l{padding-left:.25rem;padding-right:.25rem}.ph2-l{padding-left:.5rem;padding-right:.5rem}.ph3-l{padding-left:1rem;padding-right:1rem}.ph4-l{padding-left:2rem;padding-right:2rem}.ph5-l{padding-left:4rem;padding-right:4rem}.ph6-l{padding-left:8rem;padding-right:8rem}.ph7-l{padding-left:16rem;padding-right:16rem}.ma0-l{margin:0}.ma1-l{margin:.25rem}.ma2-l{margin:.5rem}.ma3-l{margin:1rem}.ma4-l{margin:2rem}.ma5-l{margin:4rem}.ma6-l{margin:8rem}.ma7-l{margin:16rem}.ml0-l{margin-left:0}.ml1-l{margin-left:.25rem}.ml2-l{margin-left:.5rem}.ml3-l{margin-left:1rem}.ml4-l{margin-left:2rem}.ml5-l{margin-left:4rem}.ml6-l{margin-left:8rem}.ml7-l{margin-left:16rem}.mr0-l{margin-right:0}.mr1-l{margin-right:.25rem}.mr2-l{margin-right:.5rem}.mr3-l{margin-right:1rem}.mr4-l{margin-right:2rem}.mr5-l{margin-right:4rem}.mr6-l{margin-right:8rem}.mr7-l{margin-right:16rem}.mb0-l{margin-bottom:0}.mb1-l{margin-bottom:.25rem}.mb2-l{margin-bottom:.5rem}.mb3-l{margin-bottom:1rem}.mb4-l{margin-bottom:2rem}.mb5-l{margin-bottom:4rem}.mb6-l{margin-bottom:8rem}.mb7-l{margin-bottom:16rem}.mt0-l{margin-top:0}.mt1-l{margin-top:.25rem}.mt2-l{margin-top:.5rem}.mt3-l{margin-top:1rem}.mt4-l{margin-top:2rem}.mt5-l{margin-top:4rem}.mt6-l{margin-top:8rem}.mt7-l{margin-top:16rem}.mv0-l{margin-top:0;margin-bottom:0}.mv1-l{margin-top:.25rem;margin-bottom:.25rem}.mv2-l{margin-top:.5rem;margin-bottom:.5rem}.mv3-l{margin-top:1rem;margin-bottom:1rem}.mv4-l{margin-top:2rem;margin-bottom:2rem}.mv5-l{margin-top:4rem;margin-bottom:4rem}.mv6-l{margin-top:8rem;margin-bottom:8rem}.mv7-l{margin-top:16rem;margin-bottom:16rem}.mh0-l{margin-left:0;margin-right:0}.mh1-l{margin-left:.25rem;margin-right:.25rem}.mh2-l{margin-left:.5rem;margin-right:.5rem}.mh3-l{margin-left:1rem;margin-right:1rem}.mh4-l{margin-left:2rem;margin-right:2rem}.mh5-l{margin-left:4rem;margin-right:4rem}.mh6-l{margin-left:8rem;margin-right:8rem}.mh7-l{margin-left:16rem;margin-right:16rem}.na1-l{margin:-.25rem}.na2-l{margin:-.5rem}.na3-l{margin:-1rem}.na4-l{margin:-2rem}.na5-l{margin:-4rem}.na6-l{margin:-8rem}.na7-l{margin:-16rem}.nl1-l{margin-left:-.25rem}.nl2-l{margin-left:-.5rem}.nl3-l{margin-left:-1rem}.nl4-l{margin-left:-2rem}.nl5-l{margin-left:-4rem}.nl6-l{margin-left:-8rem}.nl7-l{margin-left:-16rem}.nr1-l{margin-right:-.25rem}.nr2-l{margin-right:-.5rem}.nr3-l{margin-right:-1rem}.nr4-l{margin-right:-2rem}.nr5-l{margin-right:-4rem}.nr6-l{margin-right:-8rem}.nr7-l{margin-right:-16rem}.nb1-l{margin-bottom:-.25rem}.nb2-l{margin-bottom:-.5rem}.nb3-l{margin-bottom:-1rem}.nb4-l{margin-bottom:-2rem}.nb5-l{margin-bottom:-4rem}.nb6-l{margin-bottom:-8rem}.nb7-l{margin-bottom:-16rem}.nt1-l{margin-top:-.25rem}.nt2-l{margin-top:-.5rem}.nt3-l{margin-top:-1rem}.nt4-l{margin-top:-2rem}.nt5-l{margin-top:-4rem}.nt6-l{margin-top:-8rem}.nt7-l{margin-top:-16rem}.strike-l{text-decoration:line-through}.underline-l{text-decoration:underline}.no-underline-l{text-decoration:none}.tl-l{text-align:left}.tr-l{text-align:right}.tc-l{text-align:center}.tj-l{text-align:justify}.ttc-l{text-transform:capitalize}.ttl-l{text-transform:lowercase}.ttu-l{text-transform:uppercase}.ttn-l{text-transform:none}.f-6-l,.f-headline-l{font-size:6rem}.f-5-l,.f-subheadline-l{font-size:5rem}.f1-l{font-size:3rem}.f2-l{font-size:2.25rem}.f3-l{font-size:1.5rem}.f4-l{font-size:1.25rem}.f5-l{font-size:1rem}.f6-l{font-size:.875rem}.f7-l{font-size:.75rem}.measure-l{max-width:30em}.measure-wide-l{max-width:34em}.measure-narrow-l{max-width:20em}.indent-l{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-l{font-variant:small-caps}.truncate-l{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-l{margin-left:auto}.center-l,.mr-auto-l{margin-right:auto}.ml-auto-l{margin-left:auto}.clip-l{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-l{white-space:normal}.nowrap-l{white-space:nowrap}.pre-l{white-space:pre}.v-base-l{vertical-align:baseline}.v-mid-l{vertical-align:middle}.v-top-l{vertical-align:top}.v-btm-l{vertical-align:bottom}}
3 |
4 | :root {
5 | --light-gray: rgba(0, 0, 0, .1);
6 | /* --focused-input-bg-color: #FBF1A9; */
7 | }
8 |
9 | input[type="number"] {
10 | outline: 0;
11 | padding: .25rem;
12 | font-size: 1em;
13 | border: none;
14 | border: 1px solid var(--light-gray);
15 | text-align: right;
16 | transition: all .3s cubic-bezier(.645,.045,.355, 1);
17 | }
18 |
19 | input[type="number"]:focus {
20 | background-color: var(--focused-input-bg-color);
21 | }
22 |
23 | .input-group {
24 | display: flex;
25 | }
26 |
27 | .input-group input {
28 | flex: 1 1 auto;
29 | width: 100%;
30 | }
31 |
32 | .input-group-prepend, .input-group-append {
33 | display: flex;
34 | flex-direction: row;
35 | align-items: center;
36 | padding-left: 4px;
37 | padding-right: 4px;
38 | border: 1px solid var(--light-gray);
39 | }
40 |
41 | .input-group-prepend {
42 | margin-right: -1px;
43 | }
44 |
45 | .input-group-append {
46 | margin-left: -1px;
47 | }
48 |
49 | /*# sourceMappingURL=styles.css.map*/
--------------------------------------------------------------------------------
/docs/examples/spreadsheet/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: spreadsheet
7 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell
7 |
8 |
9 |
15 |
16 |
17 |
18 | xcell
19 | xcell is a tiny, open source (MIT)
20 | library for building reactive, spreadsheet-like calculations in JavaScript.
21 | Spreadsheets are cool—if we say that A1 is the sum of B1 and C1 ,
22 | the spreadsheet will automatically update whenever we change the dependent
23 | cells.
24 |
25 | This usually doesn't happen in our programs.
26 | For example in JavaScript:
27 | var b = 1 , c = 2
28 |
29 | var a = b + c
30 |
31 | b = 42
32 |
33 | alert("a is now: " + a)
34 | our variable a does not automatically change if we
35 | change b . It will be equal to 3 until we imperatively
36 | change it something else.
37 | xcell allows us to write programs that work like spreadsheets.
38 | Here is how:
39 | function add (x, y ) {
40 | return x + y
41 | }
42 |
43 | var b = xcell(1 ), c = xcell(2 )
44 |
45 | var a = xcell([b, c], add)
46 |
47 | alert(a.value)
48 |
49 | b.value = 42
50 |
51 | alert(a.value)
52 | xcell
is a function that returns an object that holds always
53 | updated value, just like a spreadsheet cell.
54 | When we create our "cells" we tell them to either be independent:
55 | var b = xcell(1 )
56 | or to depend on other cells and update its value when necessary using
57 | a provided function:
58 | var a = xcell([b, c], add)
59 | The cells emit change
event whenever they change, so we can observe
60 | them and update our UI:
61 | a.on('change' , function handleChange (sender ) {
62 | document .getElementById("my-cell" ).value = sender.value
63 | })
64 | Here are a few examples of how xcell can be used:
65 |
70 | The source code is on github .
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/docs/spreadsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomazy/xcell/f1793c6b268882d0f915ef0c5b204b380eb83e56/docs/spreadsheet.png
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | padding: 1em;
4 | color: rgba(0, 0, 0, .9);
5 | }
6 |
7 | main {
8 | font-size: 1rem;
9 | margin: auto;
10 | max-width: 34em;
11 | }
12 |
13 | main h1 {
14 | font-size: 3rem;
15 | }
16 |
17 | main p {
18 | line-height: 1.5;
19 | }
20 |
21 | main ul {
22 | line-height: 1.5;
23 | }
24 |
25 | main img {
26 | max-width: 100%;
27 | }
28 |
29 | /*
30 | Visual Studio-like style based on original C# coloring by Jason Diamond
31 | */
32 | .hljs {
33 | display: block;
34 | overflow-x: auto;
35 | padding: 0.5em;
36 | background: white;
37 | color: black;
38 | }
39 |
40 | .hljs-comment,
41 | .hljs-quote,
42 | .hljs-variable {
43 | color: #008000;
44 | }
45 |
46 | .hljs-keyword,
47 | .hljs-selector-tag,
48 | .hljs-built_in,
49 | .hljs-name,
50 | .hljs-tag {
51 | color: #00f;
52 | }
53 |
54 | .hljs-string,
55 | .hljs-title,
56 | .hljs-section,
57 | .hljs-attribute,
58 | .hljs-literal,
59 | .hljs-template-tag,
60 | .hljs-template-variable,
61 | .hljs-type,
62 | .hljs-addition {
63 | color: #a31515;
64 | }
65 |
66 | .hljs-deletion,
67 | .hljs-selector-attr,
68 | .hljs-selector-pseudo,
69 | .hljs-meta {
70 | color: #2b91af;
71 | }
72 |
73 | .hljs-doctag {
74 | color: #808080;
75 | }
76 |
77 | .hljs-attr {
78 | color: #f00;
79 | }
80 |
81 | .hljs-symbol,
82 | .hljs-bullet,
83 | .hljs-link {
84 | color: #00b0e8;
85 | }
86 |
87 |
88 | .hljs-emphasis {
89 | font-style: italic;
90 | }
91 |
92 | .hljs-strong {
93 | font-weight: bold;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/mortgage/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: mortgage
7 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/examples/mortgage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mortgage",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "dependencies": {
7 | "date-fns": "^1.29.0",
8 | "xcell": "*",
9 | "xcell-inspect": "*",
10 | "yo-yo": "^1.4.1"
11 | },
12 | "devDependencies": {
13 | "babel-loader": "^7.1.2",
14 | "html-webpack-plugin": "^2.30.1",
15 | "webpack": "^3.10.0",
16 | "webpack-dev-server": "^2.11.1"
17 | },
18 | "scripts": {
19 | "start": "webpack-dev-server",
20 | "build": "webpack -p"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/mortgage/src/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from './mortgage';
2 | import yo from 'yo-yo';
3 | import formatDate from 'date-fns/format'
4 | import parseDate from 'date-fns/parse'
5 | import inspect from 'xcell-inspect';
6 | import { Cell } from 'xcell';
7 |
8 | const store = createStore({
9 | loanAmount: 500000,
10 | rate: 1.2,
11 | loanDate: new Date(),
12 | loanTermYears: 30,
13 | })
14 |
15 | function input(cell, attrs = {}) {
16 | return yo`
17 |
18 | `
19 |
20 | function oninput(e) {
21 | cell.value = parse(e.target.value)
22 | }
23 |
24 | function parse(v) {
25 | switch (attrs.type) {
26 | case 'date':
27 | return /^\d{4}-\d{2}-\d{2}$/.test(v)
28 | ? parseDate(v)
29 | : null
30 |
31 | case 'number':
32 | return Number(v)
33 |
34 | default:
35 | return '' + v
36 | }
37 | }
38 |
39 | function format(v) {
40 | switch (attrs.type) {
41 | case 'date':
42 | return Boolean(v)
43 | ? defaultFormatDate(v)
44 | : ''
45 |
46 | case 'number':
47 | return String(v)
48 |
49 | default:
50 | return '' + v
51 | }
52 | }
53 | }
54 |
55 | function output(cell, format) {
56 | cell.on('change', cellChanged);
57 |
58 | return yo`${format(cell.value)} `;
59 |
60 | function cellChanged({id, value}) {
61 | const el = document.querySelector(`[data-cell-id="${id}"]`)
62 | el.textContent = format(value)
63 | }
64 | }
65 |
66 | function form() {
67 | return yo`
68 |
104 | `
105 | }
106 |
107 | function installments($installments) {
108 | const element = renderInstallments($installments.value);
109 |
110 | $installments.on('change', ({ value }) => {
111 | yo.update(element, renderInstallments(value))
112 | })
113 |
114 | return element;
115 |
116 | function renderInstallments(items) {
117 | return yo`
118 |
119 | ${
120 | items.length
121 | ? yo`
122 |
123 | Monthly payments
124 |
125 |
126 | #
127 | Date
128 | Interest
129 | Principal
130 | Amount
131 |
132 |
133 |
134 | ${items.map(ii => (
135 | yo`
136 |
137 |
138 | ${ii.idx + 1}
139 |
140 |
141 | ${output(ii.$date, defaultFormatDate)}
142 |
143 |
144 | ${output(ii.$interest, formatMoney)}
145 |
146 |
147 | ${output(ii.$principal, formatMoney)}
148 |
149 |
150 | ${output(ii.$amount, formatMoney)}
151 |
152 |
153 | `
154 | ))}
155 |
156 |
`
157 | : null
158 | }
159 |
160 | `
161 | }
162 | }
163 |
164 | function app() {
165 | return yo`
166 |
167 |
168 | mortgage calculator
169 | source code
170 |
171 |
172 |
173 | ${form()}
174 |
175 | ${installments(store.$installments)}
176 |
`;
177 | }
178 |
179 | document.body.appendChild(app())
180 |
181 | function formatMoney(v) {
182 | return Number.isFinite(v)
183 | ? `$${v.toFixed(2)}`
184 | : '?'
185 | }
186 |
187 | function defaultFormatDate(date) {
188 | return formatDate(date, 'YYYY-MM-DD')
189 | }
190 |
191 | function autoNameCellsForGraph(obj, prefix = '') {
192 | for (const key in obj) {
193 | if (obj[key] instanceof Cell) {
194 | obj[key].name = prefix + key
195 | }
196 | }
197 | }
198 |
199 | function autoNameInstallmentsCellsForGraph({ value }) {
200 | value.forEach((ii, idx) => {
201 | autoNameCellsForGraph(ii, `I.[${idx}].`)
202 | })
203 | }
204 |
205 | function extractCellsFromStore(store) {
206 | const result = [];
207 | for (const key in store) {
208 | if (store[key] instanceof Cell) {
209 | result.push(store[key])
210 | }
211 | }
212 | return result;
213 | }
214 |
215 | // connect the debug graph
216 | autoNameCellsForGraph(store);
217 | autoNameInstallmentsCellsForGraph(store.$installments)
218 | store.$installments.on('change', autoNameInstallmentsCellsForGraph);
219 | document.body.appendChild(
220 | inspect(
221 | extractCellsFromStore(store),
222 | {
223 | renderGraph: false,
224 | renderDOT: true,
225 | hidden: (window.innerWidth < 900) || (window.innerHeight < 700)
226 | }
227 | ).element
228 | )
229 |
--------------------------------------------------------------------------------
/examples/mortgage/src/mortgage.js:
--------------------------------------------------------------------------------
1 | import differenceInDays from 'date-fns/difference_in_days'
2 | import xcell, { Cell } from 'xcell'
3 |
4 | import {
5 | getInstallmentDate,
6 | getInstallmentInterest,
7 | getMonthlyPayment,
8 | minus,
9 | plus,
10 | sum,
11 | identity,
12 | } from "./utils";
13 |
14 | class Installment {
15 | dispose() {
16 | for (let key in this) {
17 | if (this[key] instanceof Cell) {
18 | this[key].dispose();
19 | }
20 | }
21 | }
22 | }
23 |
24 | export function createStore({ loanAmount, rate, loanDate, loanTermYears = 30 }) {
25 | const $rate = xcell(rate)
26 | const $loanAmount = xcell(loanAmount)
27 | const $loanTermYears = xcell(loanTermYears)
28 | const $loanTermMonths = xcell([$loanTermYears], x => x * 12)
29 | const $loanDate = xcell(loanDate)
30 |
31 | const $installments = xcell(
32 | [$loanTermMonths, $loanDate, $loanAmount],
33 | ( loanTermMonths, loanDate, loanAmount) => {
34 |
35 | /**
36 | * @type Installment[]
37 | */
38 | const result = []
39 |
40 | if (!loanDate || !(loanAmount > 0)) {
41 | return result;
42 | }
43 |
44 | let prev;
45 |
46 | for (let i = 0; i < loanTermMonths; i++) {
47 | void function makeInstallment(idx) {
48 | const $date = xcell([$loanDate], d => getInstallmentDate(d, idx))
49 | const $paid = prev
50 | ? xcell([prev.$paid, prev.$principal], plus)
51 | : xcell(0)
52 | const $debt = xcell([$loanAmount, $paid], minus)
53 | const $interestDays = xcell([
54 | $date,
55 | prev ? prev.$date : $loanDate
56 | ], differenceInDays)
57 | const $interest = xcell([$debt, $rate, $interestDays], getInstallmentInterest)
58 |
59 | let $principal, $amount;
60 | if (i === loanTermMonths - 1) {
61 | $principal = xcell([$debt], identity);
62 | $amount = xcell([$principal, $interest], plus);
63 | } else {
64 | const loanTerms = loanTermMonths - idx;
65 | $amount = xcell([$debt, $rate], (debt, rate) => getMonthlyPayment(debt, loanTerms, rate))
66 | $principal = xcell([$amount, $interest], minus)
67 | }
68 |
69 | const installment = new Installment()
70 | Object.assign(installment, {
71 | idx,
72 | $date,
73 | $interestDays,
74 | $debt,
75 | $paid,
76 | $interest,
77 | $principal,
78 | $amount,
79 | })
80 |
81 | result.push(installment)
82 | prev = installment;
83 | }(i)
84 | }
85 |
86 | return result;
87 | }
88 | )
89 |
90 | // cleanup
91 | $installments.on('change', (_, previous) => {
92 | for (let installment of previous) {
93 | installment.dispose();
94 | }
95 | })
96 |
97 | // dynamic ranges
98 | const $interestRange = xcell([$installments], installments =>
99 | installments.map(i => i.$interest)
100 | )
101 | const $interestSum = xcell($interestRange.value, sum)
102 | $interestRange.on('change', ({ value }) => {
103 | $interestSum.dependencies = value
104 | })
105 |
106 | const $amountRange = xcell([$installments], installments =>
107 | installments.map(i => i.$amount)
108 | )
109 | const $amountSum = xcell($amountRange.value, sum)
110 | $amountRange.on('change', ({ value }) => {
111 | $amountSum.dependencies = value
112 | })
113 |
114 | return {
115 | $rate,
116 | $loanAmount,
117 | $loanTermYears,
118 | $loanTermMonths,
119 | $loanDate,
120 | $installments,
121 | $interestSum,
122 | $amountSum,
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/examples/mortgage/src/utils.js:
--------------------------------------------------------------------------------
1 | import getDate from 'date-fns/get_date'
2 | import getMonth from 'date-fns/get_month'
3 | import getYear from 'date-fns/get_year'
4 | import lastDayOfMonth from 'date-fns/last_day_of_month'
5 |
6 | function capOnLastDayOfTheMonth(year, month, day) {
7 | const lastDay = getDate(lastDayOfMonth(new Date(year, month, 1)))
8 | return new Date(year, month, Math.min(lastDay, day))
9 | }
10 |
11 | export function getInstallmentDate(payoutDate, installmentIndex) {
12 | const year = getYear(payoutDate) + Math.floor(installmentIndex / 12)
13 | const month = getMonth(payoutDate) + installmentIndex % 12 + 1
14 | const day = getDate(payoutDate)
15 |
16 | return capOnLastDayOfTheMonth(year, month, day)
17 | }
18 |
19 | export function getInstallmentInterest(loan, interestRate, interestDays) {
20 | return loan * (interestRate / 100) * interestDays / 365
21 | }
22 |
23 | export function getMonthlyPayment(S, n, rate) {
24 | // I = S * q^n * (q-1)/(q^n-1)
25 | // I: monthly payment
26 | // S: loan amount
27 | // n: loan terms
28 | // r: rate
29 | // q: 1 + (r / 12)
30 | const r = rate / 100
31 | const q = 1 + (r / 12)
32 | const q_pow_n = Math.pow(q, n)
33 | return S * q_pow_n * (q - 1) / (q_pow_n - 1)
34 | }
35 |
36 | export function minus(a, b) {
37 | return a - b
38 | }
39 |
40 | export function plus(a, b) {
41 | return a + b
42 | }
43 |
44 | export function sum(...args) {
45 | return args.reduce((acc, e) => acc + e, 0)
46 | }
47 |
48 | export function identity(x) {
49 | return x;
50 | }
51 |
--------------------------------------------------------------------------------
/examples/mortgage/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 |
5 | module.exports = {
6 | entry: './src/index.js',
7 | output: {
8 | filename: 'bundle.js',
9 | path: __dirname + '/dist',
10 | },
11 |
12 | devtool: 'source-maps',
13 |
14 | resolve: {
15 | extensions: ['.ts', '.tsx', '.js', '.json']
16 | },
17 |
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | exclude: /node_modules/,
23 | use: {
24 | loader: 'babel-loader',
25 | options: { presets: ['es2015'] }
26 | },
27 | },
28 | ]
29 | },
30 |
31 | devServer: {
32 | contentBase: './dist'
33 | },
34 |
35 | node: {
36 | fs: 'empty',
37 | },
38 |
39 | plugins: [
40 | new CleanWebpackPlugin(['dist']),
41 | new HtmlWebpackPlugin({
42 | template: __dirname + '/index.html'
43 | })
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/examples/pizza/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: pizza
7 |
8 |
9 |
10 |
receipt
11 |
12 |
13 |
Pizza price
14 |
18 |
19 |
20 |
21 |
Tax percent
22 |
26 |
27 |
28 |
29 |
Tip percent
30 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
49 |
50 |
54 |
55 |
56 |
57 |
61 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/pizza/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pizza",
3 | "license": "MIT",
4 | "version": "0.0.0",
5 | "private": true,
6 | "devDependencies": {
7 | "babel-loader": "^7.1.2",
8 | "clean-webpack-plugin": "^0.1.17",
9 | "css-loader": "^1.0.0",
10 | "extract-text-webpack-plugin": "^3.0.2",
11 | "html-webpack-plugin": "^2.30.1",
12 | "style-loader": "^0.21.0",
13 | "webpack": "^3.10.0",
14 | "webpack-dev-server": "^2.11.0"
15 | },
16 | "scripts": {
17 | "start": "webpack-dev-server",
18 | "build": "webpack -p"
19 | },
20 | "dependencies": {
21 | "tachyons": "^4.11.1",
22 | "xcell": "*",
23 | "xcell-inspect": "*"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/pizza/src/index.js:
--------------------------------------------------------------------------------
1 | import xcell, { Cell } from 'xcell'
2 | import inspect from 'xcell-inspect'
3 |
4 | //#region -- css --
5 | import 'tachyons/css/tachyons.min.css'
6 | import './style.css'
7 | //#endregion
8 |
9 | const cells = createCells({ menuPrice: 15, taxPercent: 13, tipPercent: 15 });
10 |
11 | connectInput(MENU_PRICE, cells.$menuPrice)
12 | connectInput(TAX_PERCENT, cells.$taxPercent)
13 | connectInput(TIP_PERCENT, cells.$tipPercent)
14 |
15 | connectOutput(TAX, cells.$tax)
16 | connectOutput(TIP, cells.$tip)
17 | connectOutput(GROSS, cells.$gross)
18 | connectOutput(TOTAL, cells.$total)
19 |
20 | /**
21 | * Creates directional graph of the data with provided defaults.
22 | * @param {Object} defaults
23 | * @param {number} defaults.menuPrice
24 | * @param {number} defaults.taxPercent
25 | * @param {number} defaults.tipPercent
26 | */
27 | function createCells(defaults) {
28 | const $menuPrice = xcell(defaults.menuPrice)
29 | const $taxPercent = xcell(defaults.taxPercent)
30 | const $tipPercent = xcell(defaults.tipPercent)
31 |
32 | const $tax = xcell(
33 | [$menuPrice, $taxPercent],
34 | ( menuPrice, taxPercent) => menuPrice * taxPercent / 100,
35 | )
36 |
37 | const $gross = xcell([$menuPrice, $tax], add)
38 |
39 | const $tip = xcell(
40 | [$gross, $tipPercent],
41 | ( gross, tipPercent) => gross * tipPercent / 100,
42 | )
43 |
44 | const $total = xcell([$gross, $tip], add)
45 |
46 | return {
47 | $menuPrice,
48 | $taxPercent,
49 | $tipPercent,
50 | $tax,
51 | $tip,
52 | $gross,
53 | $total,
54 | }
55 | }
56 |
57 | //#region --- helpers ---
58 | /**
59 | * @param {Element} input
60 | * @param {Cell} cell
61 | * @param {(s: string) => any} parse
62 | */
63 | function connectInput(input, cell, parse = Number) {
64 | input.value = cell.value
65 | input.addEventListener('change', ev => {
66 | cell.value = parse(ev.target.value)
67 | })
68 | }
69 |
70 | /**
71 | * @param {Element} output
72 | * @param {Cell} cell
73 | * @param {(v: any) => string} format
74 | */
75 | function connectOutput(output, cell, format = formatMoney) {
76 | output.textContent = format(cell.value)
77 | cell.on('change', ({ value }) => {
78 | output.textContent = format(value)
79 | })
80 | }
81 |
82 | /**
83 | * @param {number} x
84 | * @param {number} y
85 | */
86 | function add(x, y) {
87 | return x + y
88 | }
89 |
90 | /**
91 | * @param {number} value
92 | */
93 | function formatMoney(value) {
94 | return value.toFixed(2)
95 | }
96 | //#endregion
97 |
98 | //#region --- inspector ---
99 | function autoNameCellsForGraph(store) {
100 | for (let key in store) {
101 | if (store[key] instanceof Cell) {
102 | store[key].name = key
103 | }
104 | }
105 | }
106 |
107 | function extractCellsFromStore(store) {
108 | const result = [];
109 | for (let key in store) {
110 | if (store[key] instanceof Cell) {
111 | result.push(store[key])
112 | }
113 | }
114 | return result;
115 | }
116 | // connect the debug inspector
117 | autoNameCellsForGraph(cells);
118 | const inspector = inspect(extractCellsFromStore(cells), {
119 | renderGraph: true,
120 | renderDOT: false,
121 | hidden: (window.innerWidth < 900) || (window.innerHeight < 700)
122 | })
123 | document.body.appendChild(inspector.element)
124 | //#endregion
125 |
--------------------------------------------------------------------------------
/examples/pizza/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-gray: rgba(0, 0, 0, .1);
3 | /* --focused-input-bg-color: #FBF1A9; */
4 | }
5 |
6 | input[type="number"] {
7 | outline: 0;
8 | padding: .25rem;
9 | font-size: 1em;
10 | border: none;
11 | border: 1px solid var(--light-gray);
12 | text-align: right;
13 | transition: all .3s cubic-bezier(.645,.045,.355, 1);
14 | }
15 |
16 | input[type="number"]:focus {
17 | background-color: var(--focused-input-bg-color);
18 | }
19 |
20 | .input-group {
21 | display: flex;
22 | }
23 |
24 | .input-group input {
25 | flex: 1 1 auto;
26 | width: 100%;
27 | }
28 |
29 | .input-group-prepend, .input-group-append {
30 | display: flex;
31 | flex-direction: row;
32 | align-items: center;
33 | padding-left: 4px;
34 | padding-right: 4px;
35 | border: 1px solid var(--light-gray);
36 | }
37 |
38 | .input-group-prepend {
39 | margin-right: -1px;
40 | }
41 |
42 | .input-group-append {
43 | margin-left: -1px;
44 | }
45 |
--------------------------------------------------------------------------------
/examples/pizza/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 |
6 | module.exports = {
7 | entry: './src/index.js',
8 | output: {
9 | filename: 'bundle.js',
10 | path: __dirname + '/dist',
11 | },
12 |
13 | devtool: 'source-maps',
14 |
15 | resolve: {
16 | extensions: ['.ts', '.tsx', '.js', '.json']
17 | },
18 |
19 | module: {
20 | rules: [
21 | {
22 | test: /\.js$/,
23 | exclude: /node_modules/,
24 | use: {
25 | loader: 'babel-loader',
26 | options: { presets: ['es2015'] }
27 | },
28 | }, {
29 | test: /\.css$/,
30 | use: ExtractTextPlugin.extract({
31 | fallback: 'style-loader',
32 | use: 'css-loader'
33 | })
34 | }
35 | ]
36 | },
37 |
38 | devServer: {
39 | contentBase: './dist'
40 | },
41 |
42 | node: {
43 | fs: 'empty',
44 | },
45 |
46 | plugins: [
47 | new CleanWebpackPlugin(['dist']),
48 | new HtmlWebpackPlugin({
49 | template: __dirname + '/index.html'
50 | }),
51 | new ExtractTextPlugin('styles.css'),
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/examples/spreadsheet/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/examples/spreadsheet/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": false,
3 | "cSpell.words": [
4 | "shallowequal",
5 | "xcell"
6 | ]
7 | }
--------------------------------------------------------------------------------
/examples/spreadsheet/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xcell: spreadsheet
7 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/examples/spreadsheet/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spreadsheet",
3 | "license": "MIT",
4 | "version": "0.0.0",
5 | "private": true,
6 | "devDependencies": {
7 | "babel-core": "^6.26.0",
8 | "babel-jest": "^22.1.0",
9 | "babel-loader": "^7.1.2",
10 | "babel-plugin-transform-es2015-parameters": "^6.24.1",
11 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
12 | "babel-plugin-yo-yoify": "^1.0.2",
13 | "babel-preset-env": "^1.6.1",
14 | "clean-webpack-plugin": "^0.1.17",
15 | "html-webpack-plugin": "^2.30.1",
16 | "jest": "^22.1.4",
17 | "standard": "^10.0.3",
18 | "webpack": "^3.10.0",
19 | "webpack-dev-server": "^2.11.0",
20 | "yo-yoify": "^4.3.0"
21 | },
22 | "dependencies": {
23 | "debug": "^3.1.0",
24 | "formula-ast": "^2.0.3",
25 | "shallowequal": "^1.0.2",
26 | "xcell": "*",
27 | "xcell-inspect": "*",
28 | "yo-yo": "^1.4.1"
29 | },
30 | "jest": {
31 | "testPathIgnorePatterns": [
32 | "node_modules"
33 | ]
34 | },
35 | "scripts": {
36 | "start": "webpack-dev-server",
37 | "build": "webpack -p",
38 | "test": "jest"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/cell-component.js:
--------------------------------------------------------------------------------
1 | import yo from 'yo-yo'
2 |
3 | const debug = require('debug')('xcell:ex:cell-component')
4 |
5 | export class CellComponent {
6 | constructor (sheet, id, onEnterPressed) {
7 | this._id = id
8 | this._sheet = sheet
9 | this._focused = false
10 | this._dirty = false
11 | this._onEnterPressed = onEnterPressed
12 | this._cellChanged = this._cellChanged.bind(this)
13 | this._setCell(sheet.getCell(id))
14 | }
15 |
16 | _updateCell (value) {
17 | this._setCell(this._sheet.setCell(this._id, String(value)))
18 | }
19 |
20 | _setCell (cell) {
21 | if (this._cell) {
22 | this._cell.removeListener('change', this._cellChanged)
23 | }
24 |
25 | this._cell = cell
26 |
27 | if (this._cell) {
28 | this._cell.addListener('change', this._cellChanged)
29 | }
30 | }
31 |
32 | _cellChanged ({ value }) {
33 | debug(`cell changed! ${this._id}`, value)
34 | this._update()
35 | }
36 |
37 | _createElement () {
38 | const oninput = (e) => {
39 | this._dirty = true
40 | }
41 |
42 | const onblur = (e) => {
43 | if (this._dirty) {
44 | this._updateCell(e.target.value)
45 | }
46 | this._focused = false
47 | this._dirty = false
48 | this._update()
49 | }
50 |
51 | const onfocus = (e) => {
52 | this._focused = true
53 | this._update()
54 | }
55 |
56 | const onkeypress = (e) => {
57 | if (e.key === 'Enter') {
58 | if (this._dirty) {
59 | this._updateCell(e.target.value)
60 | }
61 | this._onEnterPressed(this._id, e.shiftKey)
62 | }
63 | }
64 |
65 | const value = this._focused
66 | ? this._sheet.formulas[this._id] || ''
67 | : (this._cell
68 | ? String(this._cell.value)
69 | : '')
70 |
71 | return yo`
72 |
80 | `
81 | }
82 |
83 | _update () {
84 | yo.update(this.element, this._createElement())
85 | }
86 |
87 | get element () {
88 | return this.el || (this.el = this._createElement())
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/compile-formula.js:
--------------------------------------------------------------------------------
1 | import { compile } from './formula'
2 | import { functions } from './functions'
3 |
4 | /**
5 | * @param {string} formula
6 | */
7 | export function compileFormula (formula) {
8 | const result = compile(formula)
9 | if (result.type === 'value') {
10 | return result.code
11 | } else {
12 | const fn = new Function(...result.refs, `return ${result.code};`) // eslint-disable-line
13 | fn.refs = result.refs
14 | fn.ranges = result.ranges
15 | Object.assign(fn, functions)
16 | return fn
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/formula.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import * as formulaAst from 'formula-ast'
3 |
4 | export const parse = formulaAst.parse.bind(formulaAst)
5 |
6 | const INFIX_SYMBOL = {
7 | 'infix-add': '+',
8 | 'infix-subtract': '-',
9 | 'infix-multiply': '*',
10 | 'infix-divide': '/',
11 | 'infix-gt': '>',
12 | 'infix-gte': '>=',
13 | 'infix-lt': '<',
14 | 'infix-lte': '<=',
15 | 'infix-eq': '=='
16 | }
17 |
18 | export class CompilerContext {
19 | constructor () {
20 | this.refs = []
21 | this.ranges = []
22 | }
23 |
24 | ref (cell) {
25 | this.refs.push(cell)
26 | }
27 |
28 | range (tl, br) {
29 | this.ranges.push({ tl, br })
30 | }
31 | }
32 |
33 | export function compile (formula) {
34 | const ast = parse(formula)
35 | const ctx = new CompilerContext()
36 | const code = compileAst(ctx, ast)
37 | return {
38 | type: ast.type === 'value' ? 'value' : 'function',
39 | code,
40 | refs: ctx.refs,
41 | ranges: ctx.ranges
42 | }
43 | }
44 |
45 | function compileAst (ctx, node) {
46 | let children
47 | let op
48 | switch (node.type) {
49 | case 'operator':
50 | switch (node.subtype) {
51 | case 'infix-add':
52 | case 'infix-subtract':
53 | case 'infix-multiply':
54 | case 'infix-divide':
55 | case 'infix-gt':
56 | case 'infix-gte':
57 | case 'infix-lt':
58 | case 'infix-lte':
59 | case 'infix-eq':
60 | children = node.operands.map(n => compileAst(ctx, n))
61 | op = INFIX_SYMBOL[node.subtype]
62 | return '(' + children.join(op) + ')'
63 | case 'prefix-minus':
64 | return `-(${compileAst(ctx, node.operands[0])})`
65 | default:
66 | throw new Error('Unhandled operator: ' + node.subtype)
67 | }
68 |
69 | case 'cell':
70 | const addr = node.addr.toUpperCase()
71 | ctx.ref(addr)
72 | return addr
73 |
74 | case 'range':
75 | const tl = node.topLeft
76 | const br = node.bottomRight
77 | assert(tl.type === 'cell', 'expected "cell" type')
78 | assert(br.type === 'cell', 'expected "cell" type')
79 |
80 | const tlAddr = tl.addr.toUpperCase()
81 | const brAddr = br.addr.toUpperCase()
82 |
83 | const ref = `${tlAddr}_${brAddr}`
84 |
85 | ctx.range(tlAddr, brAddr)
86 | ctx.ref(ref)
87 | return ref
88 |
89 | case 'value':
90 | switch (node.subtype) {
91 | case 'array':
92 | return node.items.map(i => compileAst(ctx, i))
93 | case 'string':
94 | return JSON.stringify(node.value)
95 | default:
96 | return node.value
97 | }
98 |
99 | case 'group':
100 | return compileAst(ctx, node.exp)
101 |
102 | case 'function':
103 | const fnName = node.name.toUpperCase()
104 | const args = node.args.map(n => compileAst(ctx, n))
105 | return `arguments.callee.${fnName}(${args.join(',')})`
106 |
107 | default:
108 | throw new Error('Unhandled type: ' + node.type)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/formula.test.js:
--------------------------------------------------------------------------------
1 | /* global test,expect */
2 | /* eslint-disable no-new-func */
3 | import { compile } from './formula'
4 |
5 | /*
6 |
7 | ........................................
8 | : : A : B : C : D : E :
9 | :...:...:...:........:.............:...:
10 | : 1 : : : : : :
11 | : 2 : 1 : 2 : =A2*B2 : : :
12 | : 3 : 3 : 4 : =A3*B3 : : :
13 | : 4 : 5 : 6 : =A4*B4 : =SUM(C1:C5) : :
14 | : 5 : : : : : :
15 | :...:...:...:........:.............:...:
16 |
17 | */
18 |
19 | test('+', () => {
20 | const expr = 'A1 + A2 + A3'
21 | const result = compile(expr)
22 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
23 | expect(result.code).toEqual('((A1+A2)+A3)')
24 | })
25 |
26 | test('-', () => {
27 | const expr = 'A1 - A2 - A3'
28 | const result = compile(expr)
29 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
30 | expect(result.code).toEqual('((A1-A2)-A3)')
31 | })
32 |
33 | test('*', () => {
34 | const expr = 'A1 * A2 * A3'
35 | const result = compile(expr)
36 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
37 | expect(result.code).toEqual('((A1*A2)*A3)')
38 | })
39 |
40 | test('/', () => {
41 | const expr = 'A1 / A2 / A3'
42 | const result = compile(expr)
43 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
44 | expect(result.code).toEqual('((A1/A2)/A3)')
45 | })
46 |
47 | test('>', () => {
48 | const expr = 'A1 > A2'
49 | const result = compile(expr)
50 | expect(result.refs).toEqual(['A1', 'A2'])
51 | expect(result.code).toEqual('(A1>A2)')
52 | })
53 |
54 | test('>=', () => {
55 | const expr = 'A1 >= A2'
56 | const result = compile(expr)
57 | expect(result.refs).toEqual(['A1', 'A2'])
58 | expect(result.code).toEqual('(A1>=A2)')
59 | })
60 |
61 | test('<', () => {
62 | const expr = 'A1 < A2'
63 | const result = compile(expr)
64 | expect(result.refs).toEqual(['A1', 'A2'])
65 | expect(result.code).toEqual('(A1 {
69 | const expr = 'A1 <= A2'
70 | const result = compile(expr)
71 | expect(result.refs).toEqual(['A1', 'A2'])
72 | expect(result.code).toEqual('(A1<=A2)')
73 | })
74 |
75 | test('=', () => {
76 | const expr = 'A1 = A2'
77 | const result = compile(expr)
78 | expect(result.refs).toEqual(['A1', 'A2'])
79 | expect(result.code).toEqual('(A1==A2)')
80 | })
81 |
82 | test('*+', () => {
83 | const expr = 'A1 * A2 + A3'
84 | const result = compile(expr)
85 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
86 | expect(result.code).toEqual('((A1*A2)+A3)')
87 | })
88 |
89 | test('lowercase', () => {
90 | const expr = 'a1 * A2 + a3'
91 | const result = compile(expr)
92 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
93 | expect(result.code).toEqual('((A1*A2)+A3)')
94 | })
95 |
96 | test('SUM', () => {
97 | const expr = '=SUM(A1:a10)'
98 | const result = compile(expr)
99 | expect(result.ranges).toEqual([{ tl: 'A1', br: 'A10' }])
100 | expect(result.refs).toEqual(['A1_A10'])
101 | expect(result.code).toEqual('arguments.callee.SUM(A1_A10)')
102 | })
103 |
104 | test('IF', () => {
105 | const expr = '=IF(A1, A2, A3)'
106 | const result = compile(expr)
107 | expect(result.ranges).toEqual([])
108 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
109 | expect(result.code).toEqual('arguments.callee.IF(A1,A2,A3)')
110 | })
111 |
112 | test('IF (;)', () => {
113 | const expr = '=IF(A1; A2; A3)'
114 | const result = compile(expr)
115 | expect(result.ranges).toEqual([])
116 | expect(result.refs).toEqual(['A1', 'A2', 'A3'])
117 | expect(result.code).toEqual('arguments.callee.IF(A1,A2,A3)')
118 | })
119 |
120 | test('cell + a value', () => {
121 | const expr = 'A1 + 100'
122 | const result = compile(expr)
123 | expect(result.refs).toEqual(['A1'])
124 | expect(result.ranges).toEqual([])
125 | expect(result.code).toEqual('(A1+100)')
126 | })
127 |
128 | test('()', () => {
129 | expect(compile('(3+4)*5').code).toEqual('((3+4)*5)')
130 | })
131 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/functions.js:
--------------------------------------------------------------------------------
1 | function IF (condition, trueValue, elseValue) {
2 | return condition ? trueValue : elseValue
3 | }
4 |
5 | function ROUND (number, precision = 0) {
6 | const factor = Math.pow(10, precision)
7 | return Math.round(number * factor) / factor
8 | }
9 |
10 | function SUM (arr) {
11 | return (
12 | arr
13 | .filter(validNumber)
14 | .reduce((a, b) => a + b, 0)
15 | )
16 | }
17 |
18 | function AVERAGE (arr) {
19 | const sumCount = arr
20 | .filter(validNumber)
21 | .reduce((acc, e) => ({sum: acc.sum + e, count: acc.count + 1}), { sum: 0, count: 0 })
22 | return (sumCount.count === 0)
23 | ? '#DIV/0!'
24 | : sumCount.sum / sumCount.count
25 | }
26 |
27 | function MIN (...args) {
28 | const arr = (args.length > 1)
29 | ? args
30 | : args[0]
31 | return (
32 | arr.length > 0
33 | ? arr
34 | .filter(validNumber)
35 | .reduce((acc, e) => e < acc ? e : acc)
36 | : 0
37 | )
38 | }
39 |
40 | function MAX (...args) {
41 | const arr = (args.length > 1)
42 | ? args
43 | : args[0]
44 | return (
45 | arr.length > 0
46 | ? arr
47 | .filter(validNumber)
48 | .reduce((acc, e) => e > acc ? e : acc)
49 | : 0
50 | )
51 | }
52 |
53 | function COUNT (arr) {
54 | return (
55 | arr
56 | .filter(validNumber)
57 | .length
58 | )
59 | }
60 |
61 | export const functions = {
62 | AVERAGE,
63 | COUNT,
64 | IF,
65 | MAX,
66 | MIN,
67 | ROUND,
68 | SUM
69 | }
70 |
71 | function validNumber (n) {
72 | return (typeof n === 'number') && Number.isFinite(n)
73 | }
74 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/index.js:
--------------------------------------------------------------------------------
1 | import yo from 'yo-yo'
2 | import inspect from 'xcell-inspect'
3 | import { spreadsheet } from './spreadsheet'
4 | import { Sheet } from './sheet'
5 | import { functions } from './functions'
6 |
7 | const sheet = new Sheet()
8 | sheet.setCell('A1', '1')
9 | sheet.setCell('A2', '2')
10 | sheet.setCell('A3', '-1')
11 | sheet.setCell('A4', '4')
12 | sheet.setCell('A5', '5.2')
13 |
14 | const samples = [
15 | 'SUM(A1:A7)',
16 | 'AVERAGE(A1:A7)',
17 | 'ROUND(C2;2)',
18 | 'IF(C3 > C2; "More!"; "Less!")',
19 | 'MIN(A1:A7)',
20 | 'MAX(A1:A7)',
21 | 'COUNT(A1:A7)',
22 | 'A1 + A2 - A3 / A4',
23 | ]
24 |
25 | for (let i = 0; i < samples.length; i++) {
26 | sheet.setCell(`B${i+1}`, `${samples[i]} —>`)
27 | sheet.setCell(`C${i+1}`, `=${samples[i]}`)
28 | }
29 |
30 | const root = spreadsheet(6, samples.length + 2, sheet)
31 |
32 | const app = yo`
33 |
34 |
spreadsheet demo
35 | ${root}
36 |
37 |
Navigation: Tab/[Shift] Tab , Enter/[Shift] Enter
38 |
39 | Supported functions:
40 |
41 | ${Object.keys(functions).sort().map(n => yo`
42 | ${n}
43 | `)}
44 |
45 |
46 |
47 |
50 |
51 | `
52 | document.body.appendChild(app)
53 |
54 | const inspector = inspect(sheet.getCells(), {
55 | renderDOT: false,
56 | renderGraph: true,
57 | hidden: (window.innerWidth < 900) || (window.innerHeight < 700)
58 | })
59 | document.body.appendChild(inspector.element)
60 |
61 | sheet.on('update', () => {
62 | inspector.update(sheet.getCells())
63 | })
64 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/sheet.js:
--------------------------------------------------------------------------------
1 | import { Cell } from 'xcell'
2 | import shallowEqual from 'shallowequal'
3 | import assert from 'assert'
4 | import { EventEmitter } from 'events'
5 | import { compileFormula } from './compile-formula'
6 |
7 | const debug = require('debug')('xcell:ex:sheet')
8 |
9 | const RANGE_ADDRESS_RE = /^([A-Z]+\d+)_([A-Z]+\d+)$/
10 | const CELL_ADDRESS_RE = /^[A-Z]\d+$/
11 |
12 | export class Sheet extends EventEmitter {
13 | constructor () {
14 | super()
15 |
16 | this.$cells = new Cell({
17 | value: {},
18 | equalFunction: shallowEqual,
19 | name: 'ALL'
20 | })
21 |
22 | this.ranges = {}
23 | this.formulas = {}
24 | }
25 |
26 | /**
27 | * @param {string} id
28 | * @returns {Cell}
29 | */
30 | getCell (id) {
31 | return this.cells[id]
32 | }
33 |
34 | /**
35 | * @returns {Cell[]}
36 | */
37 | getCells () {
38 | return Object.keys(this.cells)
39 | .map(id => this.getCell(id))
40 | .filter(cell => !!cell)
41 | }
42 |
43 | /**
44 | * @private
45 | * @param {string} id
46 | * @param {string} formula
47 | * @returns {Cell}
48 | */
49 | createCellWithFormula (id, formula) {
50 | debug(`createCellWithFormula(${id}, ${formula})`)
51 | assert.equal(formula[0], '=', 'Formula must start with "="')
52 |
53 | const func = compileFormula(formula)
54 | let deps = []
55 | if (func.refs) {
56 | deps = func.refs.map(ref => (
57 | isRangeAddress(ref)
58 | ? this.ensureRange(ref)
59 | : this.ensureCellNode(ref)
60 | ))
61 | }
62 | return new Cell({
63 | name: id,
64 | deps,
65 | formula: func
66 | })
67 | }
68 |
69 | /**
70 | * @private
71 | * @param {string} begin
72 | * @param {string} end
73 | * @returns {Cell}
74 | */
75 | createRange (begin, end) {
76 | const inRange = isInRange.bind(undefined, begin, end)
77 |
78 | // $rangeDeps depends on $cells and
79 | // generates array of all the cells
80 | // in the given range.
81 | const $rangeDeps = new Cell({
82 | name: `${begin}:${end} - deps`,
83 | deps: [this.$cells],
84 | formula: (cells) => (
85 | Object.keys(cells)
86 | .filter(inRange)
87 | .map(key => cells[key])
88 | .filter(n => !!n)
89 | )
90 | })
91 |
92 | // $range depends on the cells generated above
93 | const $range = new Cell({
94 | equalFunction: shallowEqual,
95 | name: `${begin}:${end}`,
96 | deps: $rangeDeps.value,
97 | formula: (...args) => args
98 | })
99 | $range.on('change', ({ name, value }) => {
100 | debug(`range "${name}" changed`, value)
101 | })
102 |
103 | // whenever $rangeDeps changes, we update the dependencies
104 | // of the $range
105 | $rangeDeps.on('change', ({ name, value }) => {
106 | debug(`rangeHelper "${name}" changed`)
107 | $range.dependencies = value
108 | })
109 |
110 | $range.$rangeDeps = $rangeDeps
111 |
112 | return $range
113 | }
114 |
115 | /**
116 | * @param {string} id
117 | * @param {string} content
118 | * @returns {Cell}
119 | */
120 | setCell (id, content) {
121 | debug(`setValue(${id}, ${content})`)
122 | content = content.trim()
123 |
124 | const previous = this.getCell(id)
125 |
126 | if ((this.formulas[id] === content) || (content === '' && this.formulas[id] === undefined)) {
127 | return previous
128 | }
129 |
130 | let current
131 |
132 | if (content === '') {
133 | delete this.formulas[id]
134 |
135 | if (previous && previous.dependents.length > 0) {
136 | current = new Cell({
137 | value: '',
138 | name: id
139 | })
140 | } else {
141 | current = undefined
142 | }
143 | } else if (content[0] === '=') {
144 | this.formulas[id] = content
145 | current = this.createCellWithFormula(id, content)
146 | } else {
147 | this.formulas[id] = content
148 | const floatValue = parseFloat(content)
149 | const value = String(floatValue) === content ? floatValue : content
150 | if (previous && !previous.formula) {
151 | previous.value = value
152 | current = previous
153 | } else {
154 | current = new Cell({
155 | name: id,
156 | value
157 | })
158 | }
159 | }
160 |
161 | if (current !== previous) {
162 | this.cells = Object.assign({}, this.cells, { [id]: current })
163 | if (previous) {
164 | if (current) {
165 | const dependents = previous.dependents.slice()
166 | // replace dependency
167 | for (const dep of dependents) {
168 | dep.dependencies = dep.dependencies.map(
169 | d => d === previous ? current : d
170 | )
171 | }
172 | }
173 |
174 | previous.dispose()
175 | }
176 | this.gcCells()
177 | this.gcRanges()
178 | }
179 |
180 | this.emit('update')
181 |
182 | return current
183 | }
184 |
185 | /**
186 | * @private
187 | */
188 | get cells () {
189 | return this.$cells.value
190 | }
191 |
192 | /**
193 | * @private
194 | */
195 | set cells (value) {
196 | this.$cells.value = value
197 | }
198 |
199 | /**
200 | * @private
201 | */
202 | ensureCellNode (id) {
203 | const result = this.getCell(id)
204 | if (result) { return result }
205 |
206 | const cell = new Cell({ name: id, value: '' })
207 | this.cells = Object.assign({}, this.cells, { [id]: cell })
208 | return cell
209 | }
210 |
211 | /**
212 | * @private
213 | */
214 | ensureRange (address) {
215 | const result = this.ranges[address]
216 | if (result) {
217 | return result
218 | }
219 | const [, rangeBegin, rangeEnd] = RANGE_ADDRESS_RE.exec(address)
220 | return (this.ranges[address] = this.createRange(rangeBegin, rangeEnd))
221 | }
222 |
223 | /**
224 | * @private
225 | */
226 | gcCells () {
227 | const toDispose = []
228 | const keysToDelete = []
229 |
230 | Object.keys(this.cells).forEach(addr => {
231 | const cell = this.cells[addr]
232 | if (!cell) {
233 | keysToDelete.push(addr)
234 | } else if (cell.dependents.length === 0) {
235 | if (cell.value === '' && typeof cell.formula !== 'function') {
236 | toDispose.push(cell)
237 | keysToDelete.push(addr)
238 | }
239 | }
240 | })
241 |
242 | if (keysToDelete.length > 0) {
243 | const cells = Object.assign({}, this.cells)
244 | keysToDelete.forEach(key => delete cells[key])
245 | this.cells = cells
246 | }
247 |
248 | toDispose.forEach(cell => cell.dispose())
249 | }
250 |
251 | /**
252 | * @private
253 | */
254 | gcRanges () {
255 | Object.keys(this.ranges).forEach(addr => {
256 | const cell = this.ranges[addr]
257 | if (cell.dependents.length === 0) {
258 | cell.$rangeDeps.dispose()
259 | cell.$rangeDeps = null
260 | cell.dispose()
261 | delete this.ranges[addr]
262 | }
263 | })
264 | }
265 | }
266 |
267 | function isRangeAddress (address) {
268 | return RANGE_ADDRESS_RE.test(address)
269 | }
270 |
271 | const LETTERS = [
272 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
273 | 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
274 | 'U', 'V', 'W', 'X', 'Y', 'Z'].join('')
275 |
276 | export function cellAddressToCoords (addr) {
277 | return {
278 | col: LETTERS.indexOf(addr[0]) + 1,
279 | row: parseInt(addr.substr(1), 10)
280 | }
281 | }
282 |
283 | export function coordsToCellAddress (coords) {
284 | return `${LETTERS[coords.col - 1]}${coords.row}`
285 | }
286 |
287 | function isCellAddress (address) {
288 | return CELL_ADDRESS_RE.test(address)
289 | }
290 |
291 | function isInRange (begin, end, value) {
292 | if (!isCellAddress(value)) {
293 | return false
294 | }
295 | const b = cellAddressToCoords(begin)
296 | const e = cellAddressToCoords(end)
297 | const v = cellAddressToCoords(value)
298 | return (v.col >= b.col) && (v.col <= e.col) &&
299 | (v.row >= b.row) && (v.row <= e.row)
300 | }
301 |
--------------------------------------------------------------------------------
/examples/spreadsheet/src/spreadsheet.js:
--------------------------------------------------------------------------------
1 | import yo from 'yo-yo'
2 | import { cellAddressToCoords, coordsToCellAddress } from './sheet'
3 | import { CellComponent } from './cell-component'
4 |
5 | /**
6 | * Creates spreadsheet view
7 | * @param {number} cols: number of columns
8 | * @param {number} rows: number of rows
9 | * @param {Sheet} sheet: sheet
10 | * @returns {HTMLElement}
11 | */
12 | export function spreadsheet (cols, rows, sheet) {
13 | const headers = fill(cols).map((_, i) => String.fromCharCode(0x41 + i))
14 | const cellComponents = {}
15 |
16 | for (let c = 1; c <= cols; c++) {
17 | for (let r = 1; r <= rows; r++) {
18 | const id = getId(c, r)
19 | cellComponents[id] = new CellComponent(sheet, id, handleEnterPressed)
20 | }
21 | }
22 |
23 | return yo`
24 |
25 |
26 | ${' '}
27 | ${headers.map(h => yo`
28 | ${h}
29 | `)}
30 |
31 |
32 | ${fill(rows).map((_, ri) => yo`
33 |
34 | ${ri + 1}
35 | ${headers.map((col, ci) => yo`
36 | ${cellComp(ci + 1, ri + 1)}
37 | `)}
38 |
39 | `)}
40 |
41 |
42 | `
43 |
44 | function cellComp (col, row) {
45 | return cellComponents[getId(col, row)].element
46 | }
47 |
48 | function getId (c, r) {
49 | return `${headers[c - 1]}${r}`
50 | }
51 |
52 | function handleEnterPressed (address, shiftKey) {
53 | const coords = cellAddressToCoords(address)
54 | let nextAddress
55 | if (shiftKey) {
56 | if (coords.row > 1) {
57 | nextAddress = coordsToCellAddress({ ...coords, row: coords.row - 1 })
58 | }
59 | } else {
60 | if (coords.row < rows) {
61 | nextAddress = coordsToCellAddress({ ...coords, row: coords.row + 1 })
62 | }
63 | }
64 | if (nextAddress) {
65 | (window)[nextAddress].focus()
66 | }
67 | }
68 | }
69 |
70 | function fill (num) {
71 | return new Array(num).fill(void 0)
72 | }
73 |
--------------------------------------------------------------------------------
/examples/spreadsheet/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 | const CleanWebpackPlugin = require('clean-webpack-plugin')
4 |
5 | module.exports = {
6 | entry: './src/index.js',
7 | output: {
8 | filename: 'bundle.js',
9 | path: path.join(__dirname, 'dist')
10 | },
11 |
12 | devtool: 'source-maps',
13 |
14 | resolve: {
15 | extensions: ['.js', '.json']
16 | },
17 |
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | exclude: /node_modules/,
23 | use: {
24 | loader: 'babel-loader',
25 | options: {
26 | presets: ['env'],
27 | plugins: [
28 | 'babel-plugin-yo-yoify',
29 | 'transform-object-rest-spread'
30 | ]
31 | }
32 | }
33 | }
34 | ]
35 | },
36 |
37 | devServer: {
38 | contentBase: './dist'
39 | },
40 |
41 | node: {
42 | fs: 'empty'
43 | },
44 |
45 | plugins: [
46 | new CleanWebpackPlugin(['dist']),
47 | new HtmlWebpackPlugin({
48 | template: path.join(__dirname, 'index.html')
49 | })
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "lerna": "2.8.0",
3 | "packages": [
4 | "packages/*"
5 | ],
6 | "version": "0.0.10",
7 | "npmClient": "yarn"
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "private": true,
4 | "devDependencies": {
5 | "lerna": "^2.8.0",
6 | "remark-cli": "5.0.0",
7 | "remark-highlight.js": "5.0.0",
8 | "remark-html": "7.0.0",
9 | "remark-preset-lint-markdown-style-guide": "2.1.1"
10 | },
11 | "workspaces": [
12 | "packages/*",
13 | "docs/*",
14 | "examples/*"
15 | ],
16 | "scripts": {
17 | "xcell:build:for-debug": "cd packages/xcell && npm run build:for-debug",
18 | "publish": "lerna publish",
19 | "build": "lerna run build",
20 | "make-docs": "make docs"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/.npmignore:
--------------------------------------------------------------------------------
1 | tsconfig.json
2 | webpack.config.js
3 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/README.md:
--------------------------------------------------------------------------------
1 | # xcell-inspect
2 |
3 | Inspector for [xcell](https://github.com/tomazy/xcell)
4 |
5 | *Browser only*
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install xcell-inspect
11 | ```
12 |
13 | ## Usage
14 |
15 | ```html
16 |
17 |
18 |
19 | + =
20 |
21 |
43 | ```
44 |
45 | See it live on JS Bin: [demo](https://jsbin.com/humeqab/edit?output)
46 |
47 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xcell-inspect",
3 | "version": "0.0.10",
4 | "license": "MIT",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "unpkg": "dist/xcell-inspect-umd.js",
8 | "repository": "https://github.com/tomazy/xcell/tree/master/packages/xcell-inspect",
9 | "author": {
10 | "email": "tomazy@go2.pl",
11 | "name": "Tomek Maszkowski"
12 | },
13 | "description": "xcell inspector",
14 | "keywords": [
15 | "graph",
16 | "dot"
17 | ],
18 | "dependencies": {
19 | "debug": "^3.1.0",
20 | "lodash.debounce": "^4.0.8",
21 | "lodash.isequal": "^4.5.0",
22 | "viz.js": "^1.8.0",
23 | "xcell": "^0.0.10",
24 | "yo-yo": "^1.4.1"
25 | },
26 | "devDependencies": {
27 | "@types/core-js": "^0.9.46",
28 | "@types/jasmine": "^2.8.4",
29 | "@types/jest": "^22.0.1",
30 | "@types/node": "^9.3.0",
31 | "jest": "^22.1.4",
32 | "npm-run-all": "^4.1.2",
33 | "raw-loader": "^0.5.1",
34 | "ts-jest": "^22.0.1",
35 | "ts-loader": "^3.2.0",
36 | "typescript": "^2.6.2",
37 | "webpack": "^3.10.0",
38 | "worker-loader": "^1.1.0"
39 | },
40 | "jest": {
41 | "moduleFileExtensions": [
42 | "ts",
43 | "js"
44 | ],
45 | "transform": {
46 | ".(ts|tsx)": "../../node_modules/ts-jest/preprocessor.js"
47 | },
48 | "testMatch": [
49 | "**/*.test.ts"
50 | ]
51 | },
52 | "scripts": {
53 | "clean": "rm -fr dist/*",
54 | "prebuild": "npm run clean",
55 | "build": "webpack -p --progress",
56 | "build:debug": "webpack -d --progress",
57 | "test": "jest",
58 | "test:watch": "npm run test -- --watch"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/cell-to-dot-node.ts:
--------------------------------------------------------------------------------
1 | import { Cell } from 'xcell';
2 | import { Node } from './create-dot';
3 |
4 | export default function cell2node(cell: Cell): Node {
5 | const { id, name, value, dependents } = cell;
6 | const label = JSON.stringify(`${name || '#' + id}: ${valueToString(value)}`);
7 | return {
8 | id: String(id),
9 | label,
10 | dependents: dependents.map(d => String(d.id)),
11 | };
12 | }
13 |
14 | function valueToString(value: any): string {
15 | if (value === undefined) {
16 | return '??';
17 | } else if (typeof value === 'string') {
18 | if (value.length > 25) {
19 | return JSON.stringify(`${value.slice(0, 25)}...`);
20 | }
21 | return JSON.stringify(value);
22 | } else if (typeof value === 'number') {
23 | if (isInt(value)) {
24 | return String(value);
25 | } else {
26 | return value.toPrecision(8).replace(/0+$/, '');
27 | }
28 | } else if (typeof value === 'function') {
29 | return `ƒ ${value.name || 'anonymous'}()`;
30 | } else if (Array.isArray(value)) {
31 | return `Array(${value.length})`;
32 | } else {
33 | return value.toString().substring(0, 25);
34 | }
35 | }
36 |
37 | function isInt(value: number) {
38 | if (isNaN(value)) {
39 | return false;
40 | }
41 | // tslint:disable-next-line:no-bitwise
42 | return (value | 0) === value;
43 | }
44 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/create-dot.ts:
--------------------------------------------------------------------------------
1 | export type ID = string;
2 | export interface Node {
3 | id: ID;
4 | label: string;
5 | dependents: ID[];
6 | }
7 |
8 | function visitNode(node: Node, nodeDefs: string[], edgeDefs: string[]): void {
9 | const { id, label } = node;
10 | nodeDefs.push(` ${id}[label=${label}]`);
11 |
12 | if (node.dependents.length === 0) return;
13 |
14 | const ids = node.dependents.join(',');
15 | edgeDefs.push(` ${node.id} -> ${ids};`);
16 | }
17 |
18 | export default function createDOT(nodes: Node[]): string {
19 | const nodeDefs: string[] = [];
20 | const edgeDefs: string[] = [];
21 | const seen = {};
22 |
23 | for (const node of nodes) {
24 | if (seen[node.id]) continue;
25 |
26 | seen[node.id] = true;
27 | visitNode(node, nodeDefs, edgeDefs);
28 | }
29 |
30 | const content = [...nodeDefs, ...edgeDefs].join('\n');
31 |
32 | return [
33 | 'digraph {',
34 | content,
35 | '}',
36 | ].join('\n');
37 | }
38 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/deferred.ts:
--------------------------------------------------------------------------------
1 | export class Deferred {
2 | public promise: Promise;
3 | public resolve: (...args: any[]) => any;
4 | public reject: (...args: any[]) => any;
5 |
6 | constructor() {
7 | this.promise = new Promise((resolve, reject) => {
8 | this.resolve = resolve;
9 | this.reject = reject;
10 | });
11 | }
12 | }
13 |
14 | export function defer() {
15 | return new Deferred();
16 | }
17 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/dot-layout.test.ts:
--------------------------------------------------------------------------------
1 | import { parsePlain } from './dot-layout';
2 |
3 | test('graph', () => {
4 | const plain = `
5 | graph 1 3.614 4.5
6 | stop
7 | `.trim();
8 |
9 | const graph = parsePlain(plain);
10 |
11 | expect(graph.scale).toEqual(1);
12 | expect(graph.width).toEqual(3.614);
13 | expect(graph.height).toEqual(4.5);
14 | });
15 |
16 | test('node - simple', () => {
17 | const plain = `
18 | graph 1 10 10
19 | node 123 1 2 3 4 ABC solid box red yellow
20 | stop
21 | `.trim();
22 |
23 | const parsed = parsePlain(plain);
24 | expect(parsed.nodes.length).toEqual(1);
25 |
26 | const [node] = parsed.nodes;
27 |
28 | expect(node.x).toEqual(1);
29 | expect(node.y).toEqual(10 - 2);
30 | expect(node.width).toEqual(3);
31 | expect(node.height).toEqual(4);
32 | expect(node.id).toEqual('123');
33 | expect(node.label).toEqual('ABC');
34 | expect(node.style).toEqual('solid');
35 | expect(node.shape).toEqual('box');
36 | expect(node.color).toEqual('red');
37 | expect(node.fillColor).toEqual('yellow');
38 | });
39 |
40 | test('node - quotes', () => {
41 | const plain = `
42 | graph 1 3.614 4.5
43 | node 123 1.2181 4.25 1.6474 0.5 "tax rate: 0.13" solid ellipse black lightgrey
44 | stop
45 | `.trim();
46 |
47 | const parsed = parsePlain(plain);
48 | const [node] = parsed.nodes;
49 |
50 | expect(node.id).toEqual('123');
51 | expect(node.label).toEqual('tax rate: 0.13');
52 | expect(node.style).toEqual('solid');
53 | expect(node.shape).toEqual('ellipse');
54 | expect(node.color).toEqual('black');
55 | expect(node.fillColor).toEqual('lightgrey');
56 | });
57 |
58 | test('node - quotes in quotes', () => {
59 | const plain = `
60 | graph 1 3.614 4.5
61 | node 123 1.2181 4.25 1.6474 0.5 "tax rate: \\"0.13\\"" solid ellipse black lightgrey
62 | stop
63 | `.trim();
64 |
65 | const parsed = parsePlain(plain);
66 | const [node] = parsed.nodes;
67 |
68 | expect(node.id).toEqual('123');
69 | expect(node.label).toEqual('tax rate: "0.13"');
70 | expect(node.style).toEqual('solid');
71 | expect(node.shape).toEqual('ellipse');
72 | expect(node.color).toEqual('black');
73 | expect(node.fillColor).toEqual('lightgrey');
74 | });
75 |
76 | test('node - fix coords', () => {
77 | const graphH = 4.5;
78 | const plain = `
79 | graph 1 3.614 ${graphH}
80 | node 123 1.2181 4.25 1.6474 0.5 "tax rate: 0.13" solid ellipse black lightgrey
81 | stop
82 | `.trim();
83 |
84 | const parsed = parsePlain(plain);
85 | const [node] = parsed.nodes;
86 |
87 | expect(node.x).toEqual(1.2181);
88 | expect(node.y).toEqual(graphH - 4.25);
89 | expect(node.width).toEqual(1.6474);
90 | expect(node.height).toEqual(.5);
91 | });
92 |
93 | test('edge - fix coords', () => {
94 | const graphH = 4.5;
95 | const plain = `
96 | graph 1 3.614 ${graphH}
97 | edge A B 7 2.9014 3.9961 2.8487 3.7434 2.765 3.3447 2.6903 3 2.6649 2.8827 2.6367 2.7551 2.6109 2.6391 solid black
98 | stop
99 | `.trim();
100 |
101 | const parsed = parsePlain(plain);
102 | expect(parsed.edges.length).toEqual(1);
103 | const [edge] = parsed.edges;
104 |
105 | expect(edge.head).toEqual('B');
106 | expect(edge.tail).toEqual('A');
107 | expect(edge.points.length).toEqual(7);
108 | expect(edge.style).toEqual('solid');
109 | expect(edge.color).toEqual('black');
110 | });
111 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/dot-layout.ts:
--------------------------------------------------------------------------------
1 | export type ID = string;
2 |
3 | export interface DotGraph {
4 | scale: number;
5 | width: number;
6 | height: number;
7 | nodes: DotNode[];
8 | edges: DotEdge[];
9 | }
10 |
11 | export interface DotNode {
12 | id: ID;
13 | x: number;
14 | y: number;
15 | width: number;
16 | height: number;
17 | label: string;
18 | style: string;
19 | shape: string;
20 | color: string;
21 | fillColor: string;
22 | }
23 |
24 | export interface DotPoint {
25 | x: number;
26 | y: number;
27 | }
28 |
29 | export interface DotEdge {
30 | tail: string;
31 | head: string;
32 | points: DotPoint[];
33 | label?: string;
34 | labelPosition?: DotPoint;
35 | style: string;
36 | color: string;
37 | }
38 |
39 | export interface DotDiff {
40 | added: ID[];
41 | changed: ID[];
42 | }
43 |
44 | function unquote(s: string): string {
45 | if (s[0] === '"' && s[s.length - 1] === '"') {
46 | return JSON.parse(s);
47 | } else {
48 | return s;
49 | }
50 | }
51 |
52 | function tokenize(line: string): string[] {
53 | return (line.match(/"(\\.|[^"])*"|\S+/g) || [])
54 | .map(unquote);
55 | }
56 |
57 | export function parsePlain(input: string): DotGraph {
58 | const lines = input.split('\n');
59 |
60 | let result: DotGraph = {
61 | edges: [],
62 | nodes: [],
63 | height: 0,
64 | width: 0,
65 | scale: 1,
66 | };
67 |
68 | function translatePosition(p: DotPoint): DotPoint {
69 | return {
70 | x: p.x,
71 | y: result.height - p.y,
72 | };
73 | }
74 |
75 | function parseGraphLine(line: string) {
76 | const [, scale, width, height] = tokenize(line);
77 | result = {
78 | ...result,
79 | scale: parseFloat(scale),
80 | width: parseFloat(width),
81 | height: parseFloat(height),
82 | };
83 | }
84 |
85 | function parseNodeLine(line: string) {
86 | // node a 1.375 2.25 0.75 0.5 a solid ellipse black lightgrey
87 | // 0 1 2 3 4 5 6 7 8 9 10
88 | const [, id, x, y, w, h, label, style, shape, color, fillColor] = tokenize(line);
89 | const p = translatePosition({ x: parseFloat(x), y: parseFloat(y)});
90 | const node: DotNode = {
91 | ...p,
92 | id,
93 | width: parseFloat(w),
94 | height: parseFloat(h),
95 | label,
96 | style,
97 | shape,
98 | color,
99 | fillColor,
100 | };
101 | result.nodes.push(node);
102 | }
103 |
104 | function parseEdgeLine(line: string) {
105 | const terms = tokenize(line);
106 | const [, tail, head, n] = terms;
107 | const points: DotPoint[] = [];
108 |
109 | let index = 4;
110 | let pointsCount = Number(n);
111 |
112 | while (pointsCount--) {
113 | const x = Number(terms[index++]);
114 | const y = Number(terms[index++]);
115 | points.push( translatePosition({ x, y }));
116 | }
117 |
118 | const style = terms[index++];
119 | const color = terms[index++];
120 |
121 | result.edges.push({
122 | head,
123 | tail,
124 | points,
125 | style,
126 | color,
127 | });
128 | }
129 |
130 | function parseLine(line: string) {
131 | if (line === 'stop') {
132 | return;
133 | } else if (startsWith(line, 'graph')) {
134 | parseGraphLine(line);
135 | } else if (startsWith(line, 'node')) {
136 | parseNodeLine(line);
137 | } else if (startsWith(line, 'edge')) {
138 | parseEdgeLine(line);
139 | }
140 | }
141 |
142 | for (let i = 0, l = lines.length; i < l; i++) {
143 | parseLine(lines[i]);
144 | }
145 |
146 | return result;
147 | }
148 |
149 | export function diffNodes(a: DotGraph, b: DotGraph): DotDiff {
150 | const added: string[] = [];
151 | const changed: string[] = [];
152 |
153 | if (b && a) {
154 | b.nodes.forEach(bn => {
155 | let an: DotNode | undefined;
156 | if (an = a.nodes.find(n => n.id === bn.id)) {
157 | if (an.label !== bn.label) {
158 | changed.push(bn.id);
159 | }
160 | } else {
161 | added.push(bn.id);
162 | }
163 | });
164 | }
165 |
166 | return {
167 | added,
168 | changed,
169 | };
170 | }
171 |
172 | function startsWith(hay: string, needle: string) {
173 | return hay.substr(0, needle.length) === needle;
174 | }
175 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/index-umd.ts:
--------------------------------------------------------------------------------
1 | import { inspect } from './inspect';
2 |
3 | export = inspect;
4 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/index.ts:
--------------------------------------------------------------------------------
1 | import { inspect } from './inspect';
2 |
3 | export default inspect;
4 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/index.worker.ts:
--------------------------------------------------------------------------------
1 | import * as vizLite from 'viz.js/viz-lite';
2 | import { parsePlain } from './dot-layout';
3 |
4 | const ctx: Worker = self as any;
5 |
6 | const fns = {
7 | vizLite,
8 | generateDotGraph,
9 | };
10 |
11 | function generateDotGraph(digraph: string) {
12 | const plain: string = vizLite(digraph, {
13 | format: 'plain',
14 | totalMemory: 16777216 * 4, // 64MB
15 | });
16 |
17 | return parsePlain(plain);
18 | }
19 |
20 | ctx.addEventListener('message', ({ data }) => {
21 | const { fn, args, id } = data;
22 |
23 | try {
24 | const result = fns[fn](...args);
25 | ctx.postMessage({
26 | id,
27 | result,
28 | });
29 | } catch (error) {
30 | ctx.postMessage({
31 | id,
32 | error,
33 | });
34 | }
35 | });
36 |
37 | ctx.postMessage({ message: 'Hello from worker!' });
38 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/inspect.ts:
--------------------------------------------------------------------------------
1 | import debounce = require('lodash.debounce');
2 | import deepEqual = require('lodash.isequal');
3 | import xcell, { Cell } from 'xcell';
4 | import * as yo from 'yo-yo';
5 |
6 | import cell2node from './cell-to-dot-node';
7 | import createDOT from './create-dot';
8 | import { diffNodes, DotGraph } from './dot-layout';
9 | import { renderDotGraph } from './render-dot-graph';
10 | import { renderRoot } from './render-root';
11 | import { workerFunctions } from './worker-functions';
12 |
13 | // tslint:disable-next-line:no-var-requires
14 | const debug = require('debug')('xcell-inspect');
15 |
16 | const workerFns = workerFunctions();
17 |
18 | export interface Options {
19 | renderGraph: boolean;
20 | renderDOT: boolean;
21 | hidden: boolean;
22 | zoom: number;
23 | }
24 |
25 | const defaults: Options = {
26 | renderGraph: true,
27 | renderDOT: true,
28 | hidden: false,
29 | zoom: 1.0,
30 | };
31 |
32 | interface GraphInTime {
33 | graph: DotGraph | null;
34 | time: number;
35 | }
36 | export function inspect(cells: Cell[], options: Options = defaults) {
37 | const debouncedRefreshCurrentCells = debounce(refreshCurrentCells, 100, { maxWait: 1000 });
38 |
39 | const $zoom = xcell(options.zoom || 1.0);
40 | const $loading = xcell(false);
41 | const $hidden = xcell(Boolean(options.hidden));
42 |
43 | $hidden.on('change', ({ value }) => {
44 | if (!value) refreshCurrentCells();
45 | });
46 |
47 | const $lastErrorInTime = xcell({ error: null, time: 0 });
48 |
49 | const $currentCells = xcell(discoverAllCells(cells));
50 |
51 | addChangeListener($currentCells.value, cellChanged);
52 |
53 | $currentCells.on('change', ({ value }, previous: Cell[]) => {
54 | debug('$currentCells changed. Count: %d', value.length);
55 |
56 | removeChangeListener(previous, cellChanged);
57 | addChangeListener(value, cellChanged);
58 | });
59 |
60 | const $cellCount = xcell([$currentCells], cc => cc.length);
61 |
62 | const $dotNodes = xcell([$currentCells], currentCells => currentCells.map(cell2node));
63 |
64 | const $digraph = xcell([$dotNodes], createDOT);
65 | $digraph.on('change', ({ value }) => {
66 | debug('digraph', value);
67 | });
68 |
69 | const $graphInTime = xcell({ graph: null, time: 0 });
70 |
71 | const $graph = xcell([$graphInTime], ({ graph }) => graph);
72 |
73 | const $lastTwoGraphsInTime = xcell([$graphInTime], function(graphInTime) {
74 | const [, previous] = this.value || [null, { graph: null, time: null }];
75 | return [previous, graphInTime];
76 | });
77 |
78 | const $diff = new Cell({
79 | equalFunction: deepEqual,
80 | deps: [$lastTwoGraphsInTime],
81 | formula: ([a, b]) => diffNodes(a.graph, b.graph),
82 | });
83 |
84 | const $error = xcell(
85 | [$lastErrorInTime, $graphInTime],
86 | ( lastErrorInTime, graphInTime) => {
87 | if (lastErrorInTime.time > graphInTime.time) {
88 | return lastErrorInTime.error;
89 | } else {
90 | return null;
91 | }
92 | });
93 |
94 | const graphInTimeTimer = new Timer(graph => {
95 | $graphInTime.value = { graph, time: Date.now() };
96 | }, 200);
97 |
98 | xcell([$digraph, $hidden], async (digraph, hidden) => {
99 | graphInTimeTimer.stop();
100 |
101 | if (hidden) return;
102 | if (!options.renderGraph) return;
103 |
104 | const loading = smartLoading(v => $loading.value = v);
105 |
106 | try {
107 | const graph = await workerFns.generateDotGraph(digraph);
108 | $graphInTime.value = { graph, time: Date.now() };
109 | graphInTimeTimer.start(graph);
110 | } catch (error) {
111 | $lastErrorInTime.value = {
112 | error,
113 | time: Date.now(),
114 | };
115 | } finally {
116 | loading.done();
117 | }
118 | });
119 |
120 | const $graphHtml = xcell(null);
121 | xcell(
122 | [$graph, $zoom, $hidden, $diff],
123 | ( graph, zoom, hidden, diff) => {
124 |
125 | if (!graph) return;
126 | if (hidden) return;
127 |
128 | const svg = renderDotGraph({
129 | graph,
130 | zoom,
131 | ...diff,
132 | });
133 |
134 | $graphHtml.value = svg;
135 | });
136 |
137 | const $rootHtml = xcell(
138 | [$graphHtml, $error, $digraph, $cellCount, $loading, $hidden, $zoom],
139 | ( graphHtml, error, digraph, cellCount, loading, hidden, zoom) => (
140 | renderRoot({
141 | hidden,
142 | loading,
143 | zoom,
144 | cellCount,
145 | // yo-yo (morhpdom) mutates provided children nodes so we have to give it a clone here :(
146 | graph: graphHtml && graphHtml.cloneNode(true),
147 | error,
148 | dot: options.renderDOT ? digraph : null,
149 | },
150 | send,
151 | )
152 | ));
153 |
154 | const root: HTMLElement = $rootHtml.value;
155 |
156 | $rootHtml.on('change', ({ value }) => {
157 | yo.update(root, value);
158 | });
159 |
160 | return {
161 | element: root,
162 | update(newCells: Cell[]) {
163 | cells = newCells;
164 | debouncedRefreshCurrentCells();
165 | },
166 | };
167 |
168 | function cellChanged(cell: Cell) {
169 | debug('cellChanged', cell.name || '#' + cell.id);
170 | if ($hidden.value) return;
171 | debouncedRefreshCurrentCells();
172 | }
173 |
174 | function refreshCurrentCells() {
175 | debug('refreshCurrentCells');
176 | $currentCells.value = discoverAllCells(cells);
177 | }
178 |
179 | function send(action: string, ...args: any[]) {
180 | switch (action) {
181 | case 'toggleHidden':
182 | $hidden.value = !$hidden.value;
183 | break;
184 |
185 | case 'zoom':
186 | $zoom.value = args[0] as number;
187 | break;
188 |
189 | default:
190 | throw new Error(`Unknown action "${action}"`);
191 | }
192 | }
193 | }
194 |
195 | function smartLoading(cb) {
196 | let isDone = false;
197 | let triggered = false;
198 |
199 | window.setTimeout(() => {
200 | if (!isDone) {
201 | cb(true);
202 | triggered = true;
203 | }
204 | }, 300);
205 |
206 | return {
207 | done() {
208 | isDone = true;
209 | if (triggered) {
210 | cb(false);
211 | }
212 | },
213 | };
214 | }
215 |
216 | function discoverAllCells(cells: Cell[]): Cell[] {
217 | const seen = {};
218 |
219 | function visit(c: Cell) {
220 | if (seen[c.id]) return;
221 | seen[c.id] = c;
222 | for (const d of c.dependents) {
223 | visit(d);
224 | }
225 | for (const d of c.dependencies) {
226 | visit(d);
227 | }
228 | }
229 |
230 | for (const cell of cells) {
231 | visit(cell);
232 | }
233 |
234 | return Object.keys(seen).map(k => seen[k]);
235 | }
236 |
237 | function removeChangeListener(cells: Cell[], listener) {
238 | for (const cell of cells) {
239 | cell.removeListener('change', listener);
240 | }
241 | }
242 |
243 | function addChangeListener(cells: Cell[], listener) {
244 | for (const cell of cells) {
245 | cell.addListener('change', listener);
246 | }
247 | }
248 |
249 | class Timer {
250 | private handle;
251 |
252 | constructor(private fn, private ms: number) {}
253 |
254 | public stop() {
255 | if (this.handle) {
256 | window.clearTimeout(this.handle);
257 | this.handle = null;
258 | }
259 | }
260 |
261 | public start(...args) {
262 | this.stop();
263 | this.handle = window.setTimeout(() => {
264 | this.handle = null;
265 | this.fn(...args);
266 | }, this.ms);
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/render-dot-graph.ts:
--------------------------------------------------------------------------------
1 | import * as yo from 'yo-yo';
2 |
3 | import { DotEdge, DotGraph, DotNode, DotPoint } from './dot-layout';
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const debug = require('debug')('xcell-inspect:render-dot-graph');
7 |
8 | export interface Options {
9 | graph: DotGraph;
10 | zoom: number;
11 | added: string[];
12 | changed: string[];
13 | }
14 | /**
15 | *
16 | * @param graph: graph
17 | * @param id: all the nodes with IDs set disappear when we hide the graph. Having an ID on the svg fixed that.
18 | */
19 | export function renderDotGraph(options: Options): HTMLElement {
20 | const {
21 | graph,
22 | zoom,
23 | added,
24 | changed,
25 | } = options;
26 | debug('>>renderDotGraph. nodes: %d, edges: %d, added: %d, changed: %d',
27 | graph.nodes.length, graph.edges.length, added.length, changed.length);
28 |
29 | const margin = 0.05;
30 | const nodes = graph.nodes.map(renderNode);
31 | const edges = graph.edges.map(renderEdge);
32 | return yo`
33 |
39 |
40 |
50 |
51 |
52 |
53 |
54 | ${edges}
55 |
56 |
57 | ${nodes}
58 |
59 |
60 | `;
61 |
62 | function renderEdge(edge: DotEdge) {
63 | const { points } = edge;
64 | const [p0, ...bSpline] = points;
65 | const d = [moveTo(p0), bezierTo(...bSpline)].join(' ');
66 | return yo`
67 |
68 |
72 |
73 | `;
74 | }
75 |
76 | function renderNode(node: DotNode) {
77 | const { x, y, width, height, label, id } = node;
78 | const newNode = isNew(id);
79 | const changedNode = !newNode && isChanged(id);
80 |
81 | const classNames = [
82 | 'node',
83 | ... newNode ? ['new'] : [],
84 | ... changedNode ? ['changed'] : [],
85 | ].join(' ');
86 |
87 | return yo`
88 |
89 |
90 |
91 | ${label}
92 |
93 |
94 | `;
95 | }
96 |
97 | function bezierTo(...points: DotPoint[]): string {
98 | return 'C' + points.map(p => `${p.x} ${p.y}`).join(',');
99 | }
100 |
101 | function moveTo(p: DotPoint): string {
102 | return 'M' + `${p.x} ${p.y}`;
103 | }
104 |
105 | function isNew(id: string) {
106 | return added.indexOf(id) > -1;
107 | }
108 |
109 | function isChanged(id: string) {
110 | return changed.indexOf(id) > -1;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/render-root.ts:
--------------------------------------------------------------------------------
1 | import * as yo from 'yo-yo';
2 |
3 | // tslint:disable-next-line:no-var-requires
4 | export const css = require('raw-loader!./style.css');
5 |
6 | export interface State {
7 | hidden: boolean;
8 | loading: boolean;
9 | zoom: number;
10 | cellCount: number;
11 | graph?: HTMLElement;
12 | error?: string;
13 | dot?: string;
14 | }
15 |
16 | export type SendFunction = (action: string, ...args: any[]) => void;
17 |
18 | export function renderRoot(state: State, send: SendFunction): HTMLElement {
19 | const { hidden, loading, zoom, cellCount } = state;
20 |
21 | const classNames = [
22 | 'xcell-inspect',
23 | ... hidden ? ['hide'] : [],
24 | ].join(' ');
25 |
26 | return yo`
27 |
28 |
29 |
42 | ${when(hidden, null, renderContent)}
43 |
44 | `;
45 |
46 | function renderContent() {
47 | const { error, graph, dot } = state;
48 | return yo`
49 |
50 | ${when(error, () => yo`
51 |
${error}
52 | `,
53 | /* else */() => ([
54 | when(graph, () => yo`
55 |
56 | ${zoomInput()}
57 |
58 | `),
59 | when(graph, () => yo`
60 |
61 | ${graph}
62 |
63 | `),
64 | when(dot, renderDOT),
65 | ]),
66 | )}
67 |
68 | `;
69 |
70 | function renderDOT() {
71 | const MAX = 5000;
72 | const remaining = (dot as string).length - MAX;
73 |
74 | return yo`
75 |
76 |
DOT:
77 |
${(dot as string).slice(0, MAX)}${remaining > 0 ? '...' : ''}
78 | ${when(remaining > 0, () => yo`
79 |
80 |
81 | and ${remaining} characters more.
82 |
83 |
send the whole DOT to console
84 |
85 | `)}
86 |
89 |
90 | `;
91 |
92 | function logToConsole() {
93 | // tslint:disable-next-line:no-console
94 | console.log(dot);
95 | }
96 | }
97 | }
98 |
99 | function zoomInput() {
100 | return yo`
101 | send('zoom', +event.target.value)}
104 | min="0.1"
105 | max="1.0"
106 | step="0.1"
107 | value=${zoom.toFixed(1)}
108 | >`;
109 | }
110 |
111 | function when(condition, trueCase, falseCase?) {
112 | return(
113 | (condition)
114 | ? (typeof trueCase === 'function')
115 | ? trueCase()
116 | : trueCase
117 | : (typeof falseCase === 'function')
118 | ? falseCase()
119 | : falseCase
120 | );
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-gray: #ddd;
3 | --node-active-color: rgb(173, 255, 47);
4 | --node-new-color: yellow;
5 | --node-stroke-color: var(--light-gray);
6 | --node-fill-color: white;
7 | --edge-stroke-color: var(--light-gray);
8 | }
9 |
10 | .xcell-inspect {
11 | background-color: rgb(245, 245, 245);
12 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12), 0 3px 1px -2px rgba(0,0,0,.2);
13 | font-family: sans-serif;
14 | font-size: 13px;
15 | max-height: 720px;
16 | max-width: 640px;
17 | overflow: auto;
18 | position: fixed;
19 | right: 8px;
20 | top: 8px;
21 | }
22 |
23 | .xcell-inspect div {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 | .xcell-inspect .text-right {
29 | text-align: right;
30 | }
31 |
32 | .xcell-inspect .xcell-inspect-menu {
33 | padding: 4px;
34 | font-size: 12px;
35 | }
36 |
37 | .xcell-inspect .xcell-inspect-error {
38 | color: red;
39 | overflow: auto;
40 | padding: 1em;
41 | white-space: pre;
42 | }
43 |
44 | .xcell-inspect .xcell-inspect-wrapper {
45 | max-height: 640px;
46 | overflow: auto;
47 | }
48 |
49 | .xcell-inspect.hide .xcell-inspect-wrapper {
50 | display: none;
51 | }
52 |
53 | .xcell-inspect svg {
54 | background-color: white;
55 | border: 1px solid var(--light-gray);
56 | display: block;
57 | }
58 |
59 | .xcell-inspect .nodes ellipse {
60 | fill: var(--node-fill-color);
61 | stroke-width: .02px;
62 | stroke: var(--node-stroke-color);
63 | transition: fill 2s ease;
64 | }
65 |
66 | .xcell-inspect .node.changed ellipse {
67 | fill: var(--node-active-color);
68 | transition: none;
69 | }
70 |
71 | .xcell-inspect .node.new ellipse {
72 | fill: var(--node-new-color);
73 | transition: none;
74 | }
75 |
76 | .xcell-inspect .nodes rect {
77 | fill: none;
78 | stroke-width: .01px;
79 | stroke: #333;
80 | }
81 |
82 | .xcell-inspect .nodes text {
83 | font-family: monospace;
84 | font-size: .0015in
85 | }
86 |
87 | .xcell-inspect .edges path {
88 | fill: none;
89 | stroke: var(--edge-stroke-color);
90 | stroke-width: .02px;
91 | }
92 |
93 | .xcell-inspect .xcell-inspect-wrapper,
94 | .xcell-inspect .xcell-inspect-cell-count,
95 | .xcell-inspect .xcell-inspect-dot {
96 | padding: 4px;
97 | }
98 |
99 | .xcell-inspect .xcell-inspect-dot pre {
100 | background-color: white;
101 | border: 1px solid var(--light-gray);
102 | margin-bottom: 4px;
103 | margin-top: 4px;
104 | max-height: 300px;
105 | max-width: 100%;
106 | overflow: auto;
107 | }
108 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/src/worker-functions.ts:
--------------------------------------------------------------------------------
1 | import { defer, Deferred } from './deferred';
2 | import { DotGraph } from './dot-layout';
3 |
4 | // tslint:disable-next-line:no-var-requires
5 | const debug = require('debug')('xcell-inspect:worker-functions');
6 |
7 | export function workerFunctions() {
8 | const WebpackWorker = require('./index.worker');
9 | const worker = new WebpackWorker();
10 |
11 | let nextId = 1;
12 | const deferred = {};
13 |
14 | worker.addEventListener('message', ({ data }) => {
15 | const { id, result, error } = data;
16 | if (id) {
17 | const d = deferred[id] as Deferred;
18 | const elapsed = Date.now() - (d as any).start;
19 | delete deferred[id];
20 | debug('Result for id: %d. Waited %dms.', id, elapsed);
21 |
22 | if (error) {
23 | debug('ERROR!', error);
24 | d.reject(error);
25 | } else {
26 | d.resolve(result);
27 | }
28 | }
29 | });
30 |
31 | function invoke(fn: string, args: any[]) {
32 | const msg = {
33 | id: nextId++,
34 | fn,
35 | args,
36 | };
37 |
38 | debug('Invoke `%s`. Id: %d', fn, msg.id);
39 |
40 | worker.postMessage(msg);
41 |
42 | const d = deferred[msg.id] = defer();
43 | (d as any).start = Date.now();
44 | return d.promise;
45 | }
46 |
47 | function generateDotGraph(digraph: string): Promise {
48 | return invoke('generateDotGraph', [digraph]);
49 | }
50 |
51 | return {
52 | generateDotGraph,
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.common.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "removeComments": true,
6 | "noImplicitAny": false,
7 | "sourceMap": false,
8 | "target": "es2015",
9 | "types": [
10 | "node"
11 | ],
12 | "lib": [
13 | "dom",
14 | "es2015"
15 | ]
16 | },
17 | "include": [
18 | "src/**/*.ts"
19 | ],
20 | "exclude": [
21 | "node_modules",
22 | "src/**/*.test.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/xcell-inspect/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const distDir = path.join(__dirname, 'dist')
4 |
5 | const defaultConfig = (declaration) => ({
6 | resolve: {
7 | extensions: ['.ts', '.js', '.json'],
8 | },
9 |
10 | module: {
11 | rules: [{
12 | test: /\.worker\./,
13 | use: {
14 | loader: 'worker-loader',
15 | options: {
16 | inline: true,
17 | fallback: false
18 | }
19 | }
20 | }, {
21 | test: /\.ts$/,
22 | loader: 'ts-loader',
23 | options: {
24 | compilerOptions: {
25 | declaration,
26 | target: 'es5',
27 | outDir: '',
28 | }
29 | }
30 | }],
31 | },
32 |
33 | plugins: []
34 | });
35 |
36 | module.exports = [
37 | Object.assign(defaultConfig(false), {
38 | entry: './src/index-umd.ts',
39 | output: {
40 | library: 'xcellInspect',
41 | libraryTarget: 'umd',
42 | filename: 'xcell-inspect-umd.js',
43 | path: distDir,
44 | },
45 | }),
46 |
47 | Object.assign(defaultConfig(true), {
48 | entry: './src/index.ts',
49 | output: {
50 | filename: 'index.js',
51 | libraryTarget: 'commonjs2',
52 | path: distDir,
53 | },
54 | }),
55 | ]
56 |
--------------------------------------------------------------------------------
/packages/xcell/.gitignore:
--------------------------------------------------------------------------------
1 | debug
2 |
--------------------------------------------------------------------------------
/packages/xcell/.npmignore:
--------------------------------------------------------------------------------
1 | webpack.config.js
2 | tsconfig.*
3 | jest.debug.json
4 |
--------------------------------------------------------------------------------
/packages/xcell/README.md:
--------------------------------------------------------------------------------
1 | # xcell
2 |
3 | Tiny library for building reactive spreadsheet-like calculations in JavaScript.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install xcell
9 | ```
10 |
11 | ## Usage (node)
12 |
13 | ```javascript
14 | const assert = require('assert')
15 | const xcell = require('xcell')
16 |
17 | const $a = xcell(1)
18 | const $b = xcell(2)
19 | const $c = xcell([$a, $b], (a, b) => a + b)
20 |
21 | $a.value = 100
22 | assert.equals($c.value, 102) // 100 + 2
23 |
24 | $b.value = 200
25 | assert.equals($c.value, 300) // 100 + 200
26 | ```
27 |
28 | A `Cell` is an `EventEmitter`:
29 |
30 | ```javascript
31 | $c.on('change', ({ value }) => console.log(value))
32 | ```
33 |
34 | ## Usage (browser)
35 |
36 | ```html
37 |
38 | + =
39 |
53 | ```
54 |
55 | ## License
56 |
57 | MIT
58 |
--------------------------------------------------------------------------------
/packages/xcell/jest.debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "ts-jest": {
4 | "tsConfigFile": "./packages/xcell/tsconfig.debug.json"
5 | }
6 | },
7 | "moduleFileExtensions": [
8 | "ts",
9 | "js"
10 | ],
11 | "transform": {
12 | ".(ts|tsx)": "../../node_modules/ts-jest/preprocessor.js"
13 | },
14 | "testMatch": [
15 | "**/*.test.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/xcell/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xcell",
3 | "version": "0.0.10",
4 | "license": "MIT",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "unpkg": "dist/xcell-umd.js",
8 | "repository": "https://github.com/tomazy/xcell/tree/master/packages/xcell",
9 | "author": {
10 | "email": "tomazy@go2.pl",
11 | "name": "Tomek Maszkowski"
12 | },
13 | "description": "reactive cells for spreadsheet-like calculations",
14 | "keywords": [
15 | "spreadsheet",
16 | "reactive cells",
17 | "reactive programming",
18 | "directed acyclic graph",
19 | "DAG",
20 | "FRP"
21 | ],
22 | "devDependencies": {
23 | "@types/jasmine": "^2.8.4",
24 | "@types/jest": "^22.0.1",
25 | "@types/node": "^9.3.0",
26 | "jest": "^22.1.4",
27 | "npm-run-all": "^4.1.2",
28 | "ts-jest": "^22.0.1",
29 | "ts-loader": "^3.2.0",
30 | "typescript": "^2.6.2",
31 | "webpack": "^3.10.0"
32 | },
33 | "jest": {
34 | "moduleFileExtensions": [
35 | "ts",
36 | "js"
37 | ],
38 | "transform": {
39 | ".(ts|tsx)": "../../node_modules/ts-jest/preprocessor.js"
40 | },
41 | "testMatch": [
42 | "**/*.test.ts"
43 | ]
44 | },
45 | "scripts": {
46 | "clean": "rm -fr dist/* debug",
47 | "prebuild": "npm run clean",
48 | "build": "npm-run-all -p build:node build:umd",
49 | "build:node": "tsc",
50 | "build:umd": "webpack -p",
51 | "build:for-debug": "tsc -p ./tsconfig.debug.json --pretty",
52 | "test": "jest",
53 | "test:watch": "npm run test -- --watch"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/xcell/src/cell.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-console
2 | import { createCell as xcell } from './cell';
3 |
4 | test('static cell has a value', () => {
5 | const $a = xcell(123);
6 | expect($a.value).toEqual(123);
7 | });
8 |
9 | test('cells have unique ids', () => {
10 | const $a = xcell(1);
11 | const $b = xcell(2);
12 | expect($a.id).not.toEqual($b.id);
13 | });
14 |
15 | test('dynamic cell updates when deps change', () => {
16 | const $b = xcell(1);
17 | const $c = xcell(2);
18 | const $a = xcell([$b, $c], (b, c) => b + c);
19 |
20 | expect($a.value).toEqual(3);
21 |
22 | $b.value = 100;
23 | expect($a.value).toEqual(102);
24 |
25 | $c.value = 200;
26 | expect($a.value).toEqual(300);
27 | });
28 |
29 | test('dynamic cell without deps', () => {
30 | const $a = xcell([], () => 123);
31 | expect($a.value).toEqual(123);
32 | });
33 |
34 | test('glitch in diamond', () => {
35 | /*
36 | make sure that `d` is not updated with old value of one of the dependencies
37 |
38 | a
39 | / \
40 | b c
41 | \ /
42 | d
43 |
44 | */
45 | const formula = jest.fn();
46 |
47 | const $a = xcell(1);
48 | const $b = xcell([$a], a => a + 1);
49 | const $c = xcell([$a], a => a + 1);
50 | xcell([$b, $c], formula);
51 |
52 | expect(formula).toHaveBeenCalledTimes(1);
53 | formula.mockReset();
54 |
55 | $a.value = 5;
56 | expect(formula).toHaveBeenCalledTimes(1);
57 | expect(formula).toHaveBeenCalledWith(6, 6);
58 | });
59 |
60 | test('static cells emit change events', () => {
61 | const handler = jest.fn();
62 | const $a = xcell(1);
63 | $a.on('change', handler);
64 |
65 | $a.value = 3;
66 | expect(handler).toHaveBeenCalledTimes(1);
67 | expect(handler).toHaveBeenCalledWith($a, 1);
68 | handler.mockReset();
69 |
70 | $a.value = 3;
71 | expect(handler).not.toHaveBeenCalled();
72 | });
73 |
74 | test('dynamic cell emit change events', () => {
75 | const handler = jest.fn();
76 |
77 | const $a = xcell(1);
78 | const $b = xcell(2);
79 | const $c = xcell([$a, $b], (a, b) => a + b);
80 | $c.on('change', handler);
81 |
82 | expect(handler).not.toHaveBeenCalled();
83 |
84 | $a.value++;
85 | expect(handler).toHaveBeenCalledTimes(1);
86 | expect(handler).toHaveBeenCalledWith($c, 3);
87 | });
88 |
89 | test('dynamic dependencies (range)', () => {
90 | const $a1 = xcell(1);
91 | const $a2 = xcell(1);
92 |
93 | const $b1 = xcell(10);
94 | const $b2 = xcell(10);
95 |
96 | const $all = xcell({
97 | $a1,
98 | $a2,
99 | $b1,
100 | $b2,
101 | });
102 |
103 | // select only cells starting with "$a"
104 | const $rangeDeps = xcell([$all], (o) => (
105 | Object.keys(o)
106 | .filter(k => k.slice(0, 2) === '$a')
107 | .map(k => o[k])
108 | ));
109 |
110 | const $range = xcell($rangeDeps.value, (...args) => args);
111 | $rangeDeps.on('change', ({ value }) => {
112 | // we have to explicitly update the dependencies of the range
113 | $range.dependencies = value;
114 | });
115 |
116 | const $sum = xcell([$range], range => sum(...range));
117 | expect($sum.value).toBe(1 + 1);
118 |
119 | $all.value = {...$all.value, $a3: xcell(1000) };
120 | expect($sum.value).toBe(1 + 1 + 1000);
121 | });
122 |
123 | test('replacing dependencies', () => {
124 | const $a = xcell(1);
125 | const $b = xcell(2);
126 | const $c = xcell(3);
127 | const $d = xcell(4);
128 |
129 | const $sum = xcell([$a, $b], sum);
130 | expect($sum.value).toBe(1 + 2);
131 |
132 | const handler = jest.fn();
133 | $sum.on('change', handler);
134 |
135 | $sum.dependencies = [$c, $d];
136 | expect($sum.value).toBe(3 + 4);
137 | expect(handler).toHaveBeenCalledTimes(1);
138 |
139 | $c.value = 33;
140 | expect($sum.value).toBe(33 + 4);
141 | });
142 |
143 | test('disposing cells', () => {
144 | const $a = xcell(123);
145 | const $b = xcell([$a], a => a);
146 | const $c = xcell([$b], b => b);
147 |
148 | const handler = jest.fn();
149 | $b.on('change', handler);
150 |
151 | $b.dispose();
152 | expect($b.disposed).toBe(true);
153 | expect($b.dependencies.length).toBe(0);
154 | expect($b.listenerCount('change')).toBe(0);
155 | expect(handler).not.toHaveBeenCalled();
156 |
157 | // The client is responsible to remove the disposed cell
158 | // from its dependents. So here $b still is a dependency of $c.
159 | expect($c.dependencies.indexOf($b)).toBeGreaterThan(-1);
160 | });
161 |
162 | function sum(...args: number[]) {
163 | return args.reduce((acc, e) => acc + e, 0);
164 | }
165 |
--------------------------------------------------------------------------------
/packages/xcell/src/cell.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | export type Formula = (...args: any[]) => T;
4 | export type EqualFunction = (a: any, b: any) => boolean;
5 |
6 | export interface Options {
7 | value?: any;
8 | deps?: Cell[];
9 | formula?: Formula;
10 | equalFunction?: EqualFunction;
11 | name?: string; // for inspection
12 | }
13 |
14 | export class Cell extends EventEmitter {
15 | private static nextId = 1;
16 |
17 | public name?: string;
18 |
19 | private _dependencies: Cell[] = [];
20 | private _dependents: Cell[] = [];
21 | private _formula?: Formula;
22 | private _value: T;
23 | private _id: number;
24 | private _updatingWithoutDependents = false;
25 | private _disposing = false;
26 | private _disposed = false;
27 | private _equalFunction?: EqualFunction;
28 |
29 | constructor(options: Options) {
30 | super();
31 | const { value, formula, deps = [], equalFunction, name } = options;
32 | this._id = Cell.nextId++;
33 | this.name = name;
34 | this._value = value;
35 | this._formula = formula;
36 | this._equalFunction = equalFunction;
37 | this.dependencies = deps;
38 | }
39 |
40 | public dispose() {
41 | this._disposing = true;
42 | this.dependencies = [];
43 | this.removeAllListeners('change');
44 | this._disposed = true;
45 | }
46 |
47 | public named(name: string) {
48 | this.name = name;
49 | return this;
50 | }
51 |
52 | public get value(): T {
53 | return this._value;
54 | }
55 |
56 | public set value(v: T) {
57 | if (this._equalFunction) {
58 | if (this._equalFunction(this._value, v)) {
59 | return;
60 | }
61 | } else if (this._value === v) {
62 | return;
63 | }
64 | const prev = this._value;
65 | this._value = v;
66 | this.emit('change', this, prev);
67 | if (!this._updatingWithoutDependents) {
68 | this.updateDependents();
69 | }
70 | }
71 |
72 | public get id() {
73 | return this._id;
74 | }
75 |
76 | public get dependents() {
77 | return [...this._dependents];
78 | }
79 |
80 | public get dependencies() {
81 | return [...this._dependencies];
82 | }
83 |
84 | public set dependencies(v) {
85 | for (const d of this._dependencies) {
86 | d.removeDependent(this);
87 | }
88 |
89 | this._dependencies = [...v];
90 |
91 | for (const d of this._dependencies) {
92 | d.addDependent(this);
93 | }
94 |
95 | this.update();
96 | }
97 |
98 | public get formula() {
99 | return this._formula;
100 | }
101 |
102 | public get disposed() {
103 | return this._disposed;
104 | }
105 |
106 | private addDependent(cell: Cell) {
107 | this._dependents.push(cell);
108 | }
109 |
110 | private removeDependent(cell: Cell) {
111 | this._dependents = this._dependents.filter(c => c !== cell);
112 | }
113 |
114 | private update() {
115 | if (!this._formula || this._disposing || this._disposed) return;
116 | const args = this._dependencies.map(d => d.value);
117 | this.value = this._formula.apply(this, args);
118 | }
119 |
120 | private updateWithoutDependents() {
121 | this._updatingWithoutDependents = true;
122 |
123 | this.update();
124 |
125 | this._updatingWithoutDependents = false;
126 | }
127 |
128 | private updateDependents() {
129 | // depth-first search in DAG guarantees topological sort order
130 | const seen = {};
131 | const processed = {};
132 | const toUpdate: Cell[] = [];
133 | const stack: Cell[] = [this];
134 | seen[this.id] = true;
135 |
136 | while (stack.length > 0) {
137 | const c = stack[stack.length - 1]; // peek
138 | if (processed[c.id]) {
139 | const x = stack.pop() as Cell;
140 | if (x !== this) {
141 | toUpdate.push(x);
142 | }
143 | } else {
144 | for (const d of c._dependents) {
145 | if (!seen[d.id]) {
146 | seen[d.id] = true;
147 | stack.push(d);
148 | }
149 | }
150 | processed[c.id] = true;
151 | }
152 | }
153 |
154 | let l = toUpdate.length;
155 | while (l--) {
156 | toUpdate[l].updateWithoutDependents();
157 | }
158 | }
159 | }
160 |
161 | export function createCell(value: T): Cell;
162 | export function createCell(deps: [Cell], formula: (v1: U1) => T): Cell;
163 | export function createCell(deps: [Cell, Cell], formula: (v1: U1, v2: U2) => T): Cell;
164 | export function createCell(deps: [Cell, Cell, Cell], formula: (v1: U1, v2: U2, v3: U3) => T): Cell;
165 | export function createCell(deps: [Cell, Cell, Cell, Cell], formula: (v1: U1, v2: U2, v3: U3, v4: U4) => T): Cell;
166 | export function createCell(deps: [Cell, Cell, Cell, Cell, Cell], formula: (v1: U1, v2: U2, v3: U3, v4: U4, v5: U5) => T): Cell;
167 | export function createCell(deps: Cell[], formula: Formula): Cell;
168 | export function createCell(...args: any[]): Cell {
169 | let deps, formula, value;
170 |
171 | if (args.length > 1 && typeof args[1] === 'function') {
172 | deps = args[0];
173 | formula = args[1];
174 | } else {
175 | value = args[0];
176 | }
177 |
178 | return new Cell({ deps, formula, value });
179 | }
180 |
--------------------------------------------------------------------------------
/packages/xcell/src/index-umd.ts:
--------------------------------------------------------------------------------
1 | import { Cell, createCell } from './cell';
2 |
3 | (createCell as any).Cell = Cell;
4 |
5 | export = createCell;
6 |
--------------------------------------------------------------------------------
/packages/xcell/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createCell } from './cell';
2 | export { Cell, Formula } from './cell';
3 |
4 | export default createCell;
5 |
--------------------------------------------------------------------------------
/packages/xcell/tsconfig.debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.common.json",
3 | "compilerOptions": {
4 | "outDir": "debug",
5 | "removeComments": false,
6 | "sourceMap": true,
7 | "sourceRoot": "./src",
8 | "target": "es2016",
9 | "types": [
10 | "node",
11 | "jest"
12 | ]
13 | },
14 | "typeRoots": [
15 | "../../node_modules/@types"
16 | ],
17 | "include": [
18 | "src/**/*.ts"
19 | ],
20 | "exclude": [
21 | "node_modules"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/xcell/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.common.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "removeComments": true,
6 | "sourceMap": false,
7 | "target": "es5",
8 | "types": [
9 | "node"
10 | ]
11 | },
12 | "include": [
13 | "src/**/*.ts"
14 | ],
15 | "exclude": [
16 | "node_modules",
17 | "src/**/*.test.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/xcell/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const distDir = path.join(__dirname, 'dist')
4 |
5 | module.exports = {
6 | entry: './src/index-umd.ts',
7 | output: {
8 | library: 'xcell',
9 | libraryTarget: 'umd',
10 | filename: 'xcell-umd.js',
11 | path: distDir,
12 | },
13 |
14 | resolve: {
15 | extensions: ['.ts'],
16 | },
17 |
18 | module: {
19 | rules: [
20 | {
21 | test: /\.tsx?$/,
22 | loader: 'ts-loader',
23 | options: {
24 | compilerOptions: {
25 | declaration: false,
26 | }
27 | }
28 | }
29 | ]
30 | },
31 |
32 | plugins: []
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.common.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "noImplicitAny": true,
7 | "preserveConstEnums": true,
8 | "pretty": true,
9 | "sourceMap": true,
10 | "strictNullChecks": true,
11 | "target": "es5",
12 | "suppressImplicitAnyIndexErrors": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {},
7 | "rules": {
8 | "arrow-parens": false,
9 | "interface-name": [
10 | true,
11 | "never-prefix"
12 | ],
13 | "quotemark": [
14 | true,
15 | "single",
16 | "avoid-escape"
17 | ],
18 | "variable-name": [
19 | "allow-trailing-underscore"
20 | ],
21 | "object-literal-sort-keys": false,
22 | "one-variable-per-declaration": [
23 | false
24 | ],
25 | "no-conditional-assignment": false,
26 | "curly": false
27 | },
28 | "rulesDirectory": []
29 | }
30 |
--------------------------------------------------------------------------------