19 Commits 4a4cf5daa3 ... b8ffc6fe30

Author SHA1 Message Date
  Brandon Wong b8ffc6fe30 continuing work on control panel; fixed error in cell eval function 4 years ago
  Brandon Wong 70bd72fa2b added a placeholder logo 4 years ago
  Brandon Wong 7946da8e04 WIP: starting to add controls, polishing interface 4 years ago
  Brandon Wong 1a2a0f7c6a corrected typo in README 4 years ago
  Brandon Wong b2c2407e4b added a README file 4 years ago
  Brandon Wong cb1ea1101a removed old (reagent-only) frontend 4 years ago
  Brandon Wong 78f23eadc2 added handling for cell ranges 4 years ago
  Brandon Wong 7fe52628c7 added enter key effect handling 4 years ago
  Brandon Wong 8c5a4e369c removed old list-based data table and associated functions 4 years ago
  Brandon Wong 95e2c100c5 added initialization evaluation step 4 years ago
  Brandon Wong 9085ec55ab restored basic cell evaluation with simple references 4 years ago
  Brandon Wong 74e21b7631 WIP: attempting to refactor the table data (and all associated functions) to a map structure 4 years ago
  Brandon Wong b192e77a4d WIP: ported existing functionality from old frontend to re-frame 4 years ago
  Brandon Wong c3f01b6538 WIP: continuing migration to re-frame 4 years ago
  Brandon Wong eb7ff59f53 WIP: working on migrating old frontend code 5 years ago
  Brandon Wong 2ebca8839b added empty reframe frontend 5 years ago
  Brandon Wong d54415ee0d moved old frontend folder 5 years ago
  Brandon Wong e4147ec817 smart border experiment 5 years ago
  Brandon Wong ad1a998586 started error support; minor clean up 5 years ago

File diff suppressed because it is too large
+ 44 - 0
README.md


BIN
extra/logo.dia


+ 88 - 0
extra/logo.svg

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
+<svg width="26cm" height="26cm" viewBox="92 135 503 502" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs/>
+  <g id="Background">
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="152" y="193" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="248.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="303.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="358.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.85" y="193.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="248.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="303.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="358.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="413.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="468.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="523.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.85" y="578.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="413.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="468.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="523.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.7" y="578.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.8" y="138.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.65" y="138.35" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.8" y="193.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="248.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="303.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="358.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="413.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="468.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="523.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.65" y="578.35" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.6" y="138.4" width="55" height="55" rx="0" ry="0"/>
+  </g>
+</svg>

+ 14 - 17
frontend/.gitignore

@@ -1,20 +1,17 @@
-/target
-/classes
-/checkouts
-profiles.clj
-pom.xml
-pom.xml.asc
-*.jar
-*.class
+/out/
+/resources/public/js/compiled/
+/target/
+/*-init.clj
+/*.log
+
+# Leiningen
 /.lein-*
 /.nrepl-port
-/resources/public/js
-/public/js
-/out
-/.repl
-*.log
-/.env
-.rebel_readline_history
-node_modules
-dist/
+
+# Node.js dependencies
+/node_modules/
+
+# shadow-cljs cache, port files
+/.shadow-cljs/
+
 *.swp

+ 0 - 22
frontend/LICENSE

@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2014 reagent-project
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-

+ 192 - 11
frontend/README.md

@@ -1,22 +1,203 @@
+# microtables-frontend
 
-### Development mode
-To start the Figwheel compiler, navigate to the project folder and run the following command in the terminal:
+A [re-frame](https://github.com/day8/re-frame) application designed to ... well, that part is up to
+you.
 
+## Getting Started
+
+### Project Overview
+
+* Architecture:
+[Single Page Application (SPA)](https://en.wikipedia.org/wiki/Single-page_application)
+* Languages
+  - Front end ([re-frame](https://github.com/day8/re-frame)): [ClojureScript](https://clojurescript.org/) (CLJS)
+* Dependencies
+  - UI framework: [re-frame](https://github.com/day8/re-frame)
+  ([docs](https://github.com/day8/re-frame/blob/master/docs/README.md),
+  [FAQs](https://github.com/day8/re-frame/blob/master/docs/FAQs/README.md)) ->
+  [Reagent](https://github.com/reagent-project/reagent) ->
+  [React](https://github.com/facebook/react)
+* Build tools
+  - Project task & dependency management: [Leiningen](https://github.com/technomancy/leiningen)
+  - CLJS compilation, REPL, & hot reload: [`shadow-cljs`](https://github.com/thheller/shadow-cljs)
+* Development tools
+  - Debugging: [CLJS DevTools](https://github.com/binaryage/cljs-devtools)
+
+#### Directory structure
+
+* [`/`](/../../): project config files
+* [`dev/`](dev/): source files compiled only with the [dev](#running-the-app) profile
+  - [`cljs/user.cljs`](dev/cljs/user.cljs): symbols for use during development in the
+[ClojureScript REPL](#connecting-to-the-browser-repl-from-a-terminal)
+* [`resources/public/`](resources/public/): SPA root directory;
+[dev](#running-the-app) / [prod](#production) profile depends on the most recent build
+  - [`index.html`](resources/public/index.html): SPA home page
+    - Dynamic SPA content rendered in the following `div`:
+        ```html
+        <div id="app"></div>
+        ```
+    - Customizable; add headers, footers, links to other scripts and styles, etc.
+  - Generated directories and files
+    - Created on build with either the [dev](#running-the-app) or [prod](#production) profile
+    - Deleted on `lein clean` (run by all `lein` aliases before building)
+    - `js/compiled/`: compiled CLJS (`shadow-cljs`)
+      - Not tracked in source control; see [`.gitignore`](.gitignore)
+* [`src/cljs/microtables_frontend/`](src/cljs/microtables_frontend/): SPA source files (ClojureScript,
+[re-frame](https://github.com/Day8/re-frame))
+  - [`core.cljs`](src/cljs/microtables_frontend/core.cljs): contains the SPA entry point, `init`
+
+### Editor/IDE
+
+Use your preferred editor or IDE that supports Clojure/ClojureScript development. See
+[Clojure tools](https://clojure.org/community/resources#_clojure_tools) for some popular options.
+
+### Environment Setup
+
+1. Install [JDK 8 or later](https://openjdk.java.net/install/) (Java Development Kit)
+2. Install [Leiningen](https://leiningen.org/#install) (Clojure/ClojureScript project task &
+dependency management)
+3. Install [Node.js](https://nodejs.org/) (JavaScript runtime environment)
+7. Clone this repo and open a terminal in the `microtables-frontend` project root directory
+8. Download project dependencies:
+    ```sh
+    lein deps && npm install
+    ```
+
+### Browser Setup
+
+Browser caching should be disabled when developer tools are open to prevent interference with
+[`shadow-cljs`](https://github.com/thheller/shadow-cljs) hot reloading.
+
+Custom formatters must be enabled in the browser before
+[CLJS DevTools](https://github.com/binaryage/cljs-devtools) can display ClojureScript data in the
+console in a more readable way.
+
+#### Chrome/Chromium
+
+1. Open [DevTools](https://developers.google.com/web/tools/chrome-devtools/) (Linux/Windows: `F12`
+or `Ctrl-Shift-I`; macOS: `⌘-Option-I`)
+2. Open DevTools Settings (Linux/Windows: `?` or `F1`; macOS: `?` or `Fn+F1`)
+3. Select `Preferences` in the navigation menu on the left, if it is not already selected
+4. Under the `Network` heading, enable the `Disable cache (while DevTools is open)` option
+5. Under the `Console` heading, enable the `Enable custom formatters` option
+
+#### Firefox
+
+1. Open [Developer Tools](https://developer.mozilla.org/en-US/docs/Tools) (Linux/Windows: `F12` or
+`Ctrl-Shift-I`; macOS: `⌘-Option-I`)
+2. Open [Developer Tools Settings](https://developer.mozilla.org/en-US/docs/Tools/Settings)
+(Linux/macOS/Windows: `F1`)
+3. Under the `Advanced settings` heading, enable the `Disable HTTP Cache (when toolbox is open)`
+option
+
+Unfortunately, Firefox does not yet support custom formatters in their devtools. For updates, follow
+the enhancement request in their bug tracker:
+[1262914 - Add support for Custom Formatters in devtools](https://bugzilla.mozilla.org/show_bug.cgi?id=1262914).
+
+## Development
+
+### Running the App
+
+Start a temporary local web server, build the app with the `dev` profile, and serve the app with
+hot reload:
+
+```sh
+lein dev
 ```
-lein figwheel
+
+Please be patient; it may take over 20 seconds to see any output, and over 40 seconds to complete.
+
+When `[:app] Build completed` appears in the output, browse to
+[http://localhost:8280/](http://localhost:8280/).
+
+[`shadow-cljs`](https://github.com/thheller/shadow-cljs) will automatically push ClojureScript code
+changes to your browser on save. To prevent a few common issues, see
+[Hot Reload in ClojureScript: Things to avoid](https://code.thheller.com/blog/shadow-cljs/2019/08/25/hot-reload-in-clojurescript.html#things-to-avoid).
+
+Opening the app in your browser starts a
+[ClojureScript browser REPL](https://clojurescript.org/reference/repl#using-the-browser-as-an-evaluation-environment),
+to which you may now connect.
+
+#### Connecting to the browser REPL from your editor
+
+See
+[Shadow CLJS User's Guide: Editor Integration](https://shadow-cljs.github.io/docs/UsersGuide.html#_editor_integration).
+Note that `lein dev` runs `shadow-cljs watch` for you, and that this project's running build id is
+`app`, or the keyword `:app` in a Clojure context.
+
+Alternatively, search the web for info on connecting to a `shadow-cljs` ClojureScript browser REPL
+from your editor and configuration.
+
+For example, in Vim / Neovim with `fireplace.vim`
+1. Open a `.cljs` file in the project to activate `fireplace.vim`
+2. In normal mode, execute the `Piggieback` command with this project's running build id, `:app`:
+    ```vim
+    :Piggieback :app
+    ```
+
+#### Connecting to the browser REPL from a terminal
+
+1. Connect to the `shadow-cljs` nREPL:
+    ```sh
+    lein repl :connect localhost:8777
+    ```
+    The REPL prompt, `shadow.user=>`, indicates that is a Clojure REPL, not ClojureScript.
+
+2. In the REPL, switch the session to this project's running build id, `:app`:
+    ```clj
+    (shadow.cljs.devtools.api/nrepl-select :app)
+    ```
+    The REPL prompt changes to `cljs.user=>`, indicating that this is now a ClojureScript REPL.
+3. See [`user.cljs`](dev/cljs/user.cljs) for symbols that are immediately accessible in the REPL
+without needing to `require`.
+
+### Running `shadow-cljs` Actions
+
+See a list of [`shadow-cljs CLI`](https://shadow-cljs.github.io/docs/UsersGuide.html#_command_line)
+actions:
+```sh
+lein run -m shadow.cljs.devtools.cli --help
 ```
 
-Figwheel will automatically push cljs changes to the browser.
-Once Figwheel starts up, you should be able to open the `public/index.html` page in the browser.
+Please be patient; it may take over 10 seconds to see any output. Also note that some actions shown
+may not actually be supported, outputting "Unknown action." when run.
+
+Run a shadow-cljs action on this project's build id (without the colon, just `app`):
+```sh
+lein run -m shadow.cljs.devtools.cli <action> app
+```
+### Debug Logging
 
-### REPL
+The `debug?` variable in [`config.cljs`](src/cljs/microtables_frontend/config.cljs) defaults to `true` in
+[`dev`](#running-the-app) builds, and `false` in [`prod`](#production) builds.
 
-The project is setup to start nREPL on port `7002` once Figwheel starts.
-Once you connect to the nREPL, run `(cljs)` to switch to the ClojureScript REPL.
+Use `debug?` for logging or other tasks that should run only on `dev` builds:
 
-### Building for production
+```clj
+(ns microtables-frontend.example
+  (:require [microtables-frontend.config :as config])
 
+(when config/debug?
+  (println "This message will appear in the browser console only on dev builds."))
 ```
-lein clean
-lein package
+
+## Production
+
+Build the app with the `prod` profile:
+
+```sh
+lein prod
 ```
+
+Please be patient; it may take over 15 seconds to see any output, and over 30 seconds to complete.
+
+The `resources/public/js/compiled` directory is created, containing the compiled `app.js` and
+`manifest.edn` files.
+
+The [`resources/public`](resources/public/) directory contains the complete, production web front
+end of your app.
+
+Always inspect the `resources/public/js/compiled` directory prior to deploying the app. Running any
+`lein` alias in this project after `lein dev` will, at the very least, run `lein clean`, which
+deletes this generated directory. Further, running `lein dev` will generate many, much larger
+development versions of the files in this directory.

+ 8 - 0
frontend/dev/cljs/user.cljs

@@ -0,0 +1,8 @@
+(ns cljs.user
+  "Commonly used symbols for easy access in the ClojureScript REPL during
+  development."
+  (:require
+    [cljs.repl :refer (Error->map apropos dir doc error->str ex-str ex-triage
+                       find-doc print-doc pst source)]
+    [clojure.pprint :refer (pprint)]
+    [clojure.string :as str]))

+ 0 - 11
frontend/env/dev/clj/user.clj

@@ -1,11 +0,0 @@
-(ns user
- (:require [figwheel-sidecar.repl-api :as ra]))
-
-(defn start-fw []
- (ra/start-figwheel!))
-
-(defn stop-fw []
- (ra/stop-figwheel!))
-
-(defn cljs []
- (ra/cljs-repl))

+ 0 - 15
frontend/env/dev/cljs/microtables_frontend/dev.cljs

@@ -1,15 +0,0 @@
-(ns ^:figwheel-no-load microtables-frontend.dev
-  (:require
-    [microtables-frontend.core :as core]
-    [devtools.core :as devtools]))
-
-(extend-protocol IPrintWithWriter
-  js/Symbol
-  (-pr-writer [sym writer _]
-    (-write writer (str "\"" (.toString sym) "\""))))
-
-(enable-console-print!)
-
-(devtools/install!)
-
-(core/init!)

+ 0 - 8
frontend/env/prod/cljs/microtables_frontend/prod.cljs

@@ -1,8 +0,0 @@
-(ns microtables-frontend.prod
-  (:require
-    [microtables-frontend.core :as core]))
-
-;;ignore println statements in prod
-(set! *print-fn* (fn [& _]))
-
-(core/init!)

+ 28 - 0
frontend/karma.conf.js

@@ -0,0 +1,28 @@
+module.exports = function (config) {
+  var junitOutputDir = process.env.CIRCLE_TEST_REPORTS || "target/junit"
+
+  config.set({
+    browsers: ['ChromeHeadless'],
+    basePath: 'target',
+    files: ['karma-test.js'],
+    frameworks: ['cljs-test'],
+    plugins: [
+        'karma-cljs-test',
+        'karma-chrome-launcher',
+        'karma-junit-reporter'
+    ],
+    colors: true,
+    logLevel: config.LOG_INFO,
+    client: {
+      args: ['shadow.test.karma.init'],
+      singleRun: true
+    },
+
+    // the default configuration
+    junitReporter: {
+      outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml
+      outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile
+      suite: '' // suite will become the package name attribute in xml testsuite element
+    }
+  })
+}

+ 992 - 0
frontend/package-lock.json

@@ -0,0 +1,992 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "assert": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
+      "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.1.1",
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "async-limiter": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+      "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
+      "dev": true
+    },
+    "bn.js": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz",
+      "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==",
+      "dev": true
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "^1.0.4",
+        "browserify-des": "^1.0.0",
+        "evp_bytestokey": "^1.0.0"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+      "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "des.js": "^1.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "randombytes": "^2.0.1"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "browserify-sign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz",
+      "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^5.1.1",
+        "browserify-rsa": "^4.0.1",
+        "create-hash": "^1.2.0",
+        "create-hmac": "^1.1.7",
+        "elliptic": "^6.5.2",
+        "inherits": "^2.0.4",
+        "parse-asn1": "^5.1.5",
+        "readable-stream": "^3.6.0",
+        "safe-buffer": "^5.2.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.2.1",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+          "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+          "dev": true
+        }
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
+    "buffer": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+      "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
+      }
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "complex.js": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz",
+      "integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw=="
+    },
+    "console-browserify": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+      "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
+      "dev": true
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "elliptic": "^6.0.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "^1.0.0",
+        "browserify-sign": "^4.0.0",
+        "create-ecdh": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.0",
+        "diffie-hellman": "^5.0.0",
+        "inherits": "^2.0.1",
+        "pbkdf2": "^3.0.3",
+        "public-encrypt": "^4.0.0",
+        "randombytes": "^2.0.0",
+        "randomfill": "^1.0.3"
+      }
+    },
+    "decimal.js": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz",
+      "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw=="
+    },
+    "des.js": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
+      "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "miller-rabin": "^4.0.0",
+        "randombytes": "^2.0.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "domain-browser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+      "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+      "dev": true
+    },
+    "elliptic": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
+      "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "escape-latex": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
+      "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
+    },
+    "events": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
+      "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
+      "dev": true
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "fraction.js": {
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.12.tgz",
+      "integrity": "sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA=="
+    },
+    "hash-base": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
+      "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.6.0",
+        "safe-buffer": "^5.2.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.2.1",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+          "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+          "dev": true
+        }
+      }
+    },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
+    },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
+      "dev": true
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "javascript-natural-sort": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+      "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "mathjs": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-7.0.1.tgz",
+      "integrity": "sha512-ikFnvtvui8EA1KC+RsF7Sse34WA7EGsKnwwv7/lTRx04t25JtWpVWrs0ZcNKxygZVrOIpU9MRgbvXEFYFV3pOQ==",
+      "requires": {
+        "complex.js": "^2.0.11",
+        "decimal.js": "^10.2.0",
+        "escape-latex": "^1.2.0",
+        "fraction.js": "^4.0.12",
+        "javascript-natural-sort": "^0.7.1",
+        "seed-random": "^2.2.0",
+        "tiny-emitter": "^2.1.0",
+        "typed-function": "^1.1.1"
+      }
+    },
+    "md5.js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "brorand": "^1.0.1"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "node-libs-browser": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
+      "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==",
+      "dev": true,
+      "requires": {
+        "assert": "^1.1.1",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^4.3.0",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "crypto-browserify": "^3.11.0",
+        "domain-browser": "^1.1.1",
+        "events": "^3.0.0",
+        "https-browserify": "^1.0.0",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "0.0.1",
+        "process": "^0.11.10",
+        "punycode": "^1.2.4",
+        "querystring-es3": "^0.2.0",
+        "readable-stream": "^2.3.3",
+        "stream-browserify": "^2.0.1",
+        "stream-http": "^2.7.2",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "^0.11.0",
+        "util": "^0.11.0",
+        "vm-browserify": "^1.0.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "dev": true
+    },
+    "parse-asn1": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
+      "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==",
+      "dev": true,
+      "requires": {
+        "asn1.js": "^4.0.0",
+        "browserify-aes": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.0",
+        "pbkdf2": "^3.0.3",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "path-browserify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
+      "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
+      "dev": true
+    },
+    "pbkdf2": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
+      "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
+      "dev": true,
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true
+    },
+    "prop-types": {
+      "version": "15.7.2",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+      "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.8.1"
+      }
+    },
+    "public-encrypt": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+      "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "parse-asn1": "^5.0.0",
+        "randombytes": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+          "dev": true
+        }
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+      "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+      "dev": true,
+      "requires": {
+        "randombytes": "^2.0.5",
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "react": {
+      "version": "16.9.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.9.0.tgz",
+      "integrity": "sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.2"
+      }
+    },
+    "react-dom": {
+      "version": "16.9.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
+      "integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.2",
+        "scheduler": "^0.15.0"
+      }
+    },
+    "react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
+    "readable-stream": {
+      "version": "2.3.7",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+      "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+      "dev": true,
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readline-sync": {
+      "version": "1.4.10",
+      "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
+      "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
+      "dev": true
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "scheduler": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
+      "integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1"
+      }
+    },
+    "seed-random": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz",
+      "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ="
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shadow-cljs": {
+      "version": "2.8.83",
+      "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.8.83.tgz",
+      "integrity": "sha512-oqqSLARvYXopA9QLf5znrguvJOSRm65LYL9XHuRWaUbMGXlygqgCjSFgW1yREXmqUQ+i2TLoA1zulYO6nTTy6g==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1",
+        "node-libs-browser": "^2.0.0",
+        "readline-sync": "^1.4.7",
+        "shadow-cljs-jar": "1.3.1",
+        "source-map-support": "^0.4.15",
+        "which": "^1.3.1",
+        "ws": "^3.0.0"
+      },
+      "dependencies": {
+        "source-map-support": {
+          "version": "0.4.18",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+          "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+          "dev": true,
+          "requires": {
+            "source-map": "^0.5.6"
+          }
+        }
+      }
+    },
+    "shadow-cljs-jar": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/shadow-cljs-jar/-/shadow-cljs-jar-1.3.1.tgz",
+      "integrity": "sha512-IJSm4Gfu/wWDsOQ0wNrSxuaGdjzsd78us+3bop3cpWsoO2Igdu6VIBItYrZHRRBKl5LIZKXfnSh/2eWG3C1EFw==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "stream-browserify": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
+      "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
+      "dev": true,
+      "requires": {
+        "inherits": "~2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "stream-http": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+      "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.6",
+        "to-arraybuffer": "^1.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "timers-browserify": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz",
+      "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==",
+      "dev": true,
+      "requires": {
+        "setimmediate": "^1.0.4"
+      }
+    },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "typed-function": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-1.1.1.tgz",
+      "integrity": "sha512-RbN7MaTQBZLJYzDENHPA0nUmWT0Ex80KHItprrgbTPufYhIlTePvCXZxyQK7wgn19FW5bnuaBIKcBb5mRWjB1Q=="
+    },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "util": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
+      "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+          "dev": true
+        }
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "vm-browserify": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+      "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+      "dev": true
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "dev": true,
+      "requires": {
+        "async-limiter": "~1.0.0",
+        "safe-buffer": "~5.1.0",
+        "ultron": "~1.1.0"
+      }
+    },
+    "xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true
+    }
+  }
+}

+ 7 - 15
frontend/package.json

@@ -1,18 +1,10 @@
 {
-  "name": "microtables-frontend",
-  "author": "Brandon Wong <projects@brwong.net> (https://www.brwong.net)",
-  "description": "Frontend of the Microtables project",
-  "version": "1.0.0",
-  "main": "index.js",
-  "dependencies": {
-    "mathjs": "6.2.3",
-    "webpack": "4.41.2",
-    "webpack-cli": "3.3.9"
-  },
-  "devDependencies": {},
-  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+  "devDependencies": {
+    "shadow-cljs": "2.8.83"
   },
-  "keywords": [],
-  "license": "ISC"
+  "dependencies": {
+    "mathjs": "7.0.1",
+    "react": "16.9.0",
+    "react-dom": "16.9.0"
+  }
 }

+ 41 - 57
frontend/project.clj

@@ -1,59 +1,43 @@
 (defproject microtables-frontend "0.1.0-SNAPSHOT"
-  :description "FIXME: write description"
-  :url "http://example.com/FIXME"
-  :license {:name "Eclipse Public License"
-            :url "http://www.eclipse.org/legal/epl-v10.html"}
-
   :dependencies [[org.clojure/clojure "1.10.1"]
-                 [org.clojure/clojurescript "1.10.520"]
-                 [reagent "0.8.1"]]
-
-  :plugins [[lein-cljsbuild "1.1.7"]
-            [lein-figwheel "0.5.19"]]
-
-  :clean-targets ^{:protect false}
-
-  [:target-path
-   [:cljsbuild :builds :app :compiler :output-dir]
-   [:cljsbuild :builds :app :compiler :output-to]]
-
-  :resource-paths ["public"]
-
-
-  :figwheel {:http-server-root "."
-             :nrepl-port 7002
-             :nrepl-middleware [cider.piggieback/wrap-cljs-repl]
-             :css-dirs ["public/css"]}
-
-  :cljsbuild {:builds {:app
-                       {:source-paths ["src" "env/dev/cljs"]
-                        :compiler
-                        {:main "microtables-frontend.dev"
-                         :output-to "public/js/app.js"
-                         :output-dir "public/js/out"
-                         :asset-path   "js/out"
-                         :source-map true
-                         :npm-deps false
-                         :optimizations :none
-                         :pretty-print  true
-                         :foreign-libs [{:file "dist/index.bundle.js"
-                                         :provides ["mathjs"]
-                                         :global-exports {mathjs mathjs}}]}
-                        :figwheel
-                        {:on-jsload "microtables-frontend.core/mount-root"}}
-                       :release
-                       {:source-paths ["src" "env/prod/cljs"]
-                        :compiler
-                        {:output-to "public/js/app.js"
-                         :output-dir "public/js/release"
-                         :optimizations :advanced
-                         :infer-externs true
-                         :pretty-print false}}}}
-
-  :aliases {"package" ["do" "clean" ["cljsbuild" "once" "release"]]}
-
-  :profiles {:dev {:source-paths ["src" "env/dev/clj"]
-                   :dependencies [[binaryage/devtools "0.9.10"]
-                                  [figwheel-sidecar "0.5.19"]
-                                  [nrepl "0.6.0"]
-                                  [cider/piggieback "0.4.1"]]}})
+                 [org.clojure/clojurescript "1.10.597"
+                  :exclusions [com.google.javascript/closure-compiler-unshaded
+                               org.clojure/google-closure-library
+                               org.clojure/google-closure-library-third-party]]
+                 [thheller/shadow-cljs "2.8.83"]
+                 [reagent "0.9.1"]
+                 [re-frame "0.11.0"]]
+
+  :plugins [
+            [lein-shell "0.5.0"]]
+
+  :min-lein-version "2.5.3"
+
+  :source-paths ["src/clj" "src/cljs"]
+
+  :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
+
+
+  :shell {:commands {"open" {:windows ["cmd" "/c" "start"]
+                             :macosx  "open"
+                             :linux   "xdg-open"}}}
+
+  :aliases {"dev"          ["with-profile" "dev" "do"
+                            ["run" "-m" "shadow.cljs.devtools.cli" "watch" "app"]]
+            "prod"         ["with-profile" "prod" "do"
+                            ["run" "-m" "shadow.cljs.devtools.cli" "release" "app"]]
+            "build-report" ["with-profile" "prod" "do"
+                            ["run" "-m" "shadow.cljs.devtools.cli" "run" "shadow.cljs.build-report" "app" "target/build-report.html"]
+                            ["shell" "open" "target/build-report.html"]]
+            "karma"        ["with-profile" "prod" "do"
+                            ["run" "-m" "shadow.cljs.devtools.cli" "compile" "karma-test"]
+                            ["shell" "karma" "start" "--single-run" "--reporters" "junit,dots"]]}
+
+  :profiles
+  {:dev
+   {:dependencies [[binaryage/devtools "1.0.0"]]
+    :source-paths ["dev"]}
+
+   :prod { }}
+
+  :prep-tasks [])

+ 0 - 67
frontend/public/css/site.css

@@ -1,67 +0,0 @@
-body {
-  font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif;
-  max-width: 600px;
-  margin: 0 auto;
-  padding-top: 72px;
-  -webkit-font-smoothing: antialiased;
-  font-size: 1.125em;
-  color: #333;
-  line-height: 1.5em;
-}
-
-h1, h2, h3 {
-  color: #000;
-}
-h1 {
-  font-size: 2.5em
-}
-
-h2 {
-  font-size: 2em
-}
-
-h3 {
-  font-size: 1.5em
-}
-
-a {
-  text-decoration: none;
-  color: #09f;
-}
-
-a:hover {
-  text-decoration: underline;
-}
-
-table {
-    border-collapse: collapse;
-}
-
-td, th {
-    border: 1px solid black;
-    padding: 0;
-}
-th {
-    text-align: center;
-    min-width: 40px;
-}
-td {
-    text-align: right;
-}
-
-td input {
-    border: none;
-    padding: 5px;
-    width: 80px;
-}
-td input:hover {
-    background-color: #ccc;
-}
-td input:focus {
-    background-color: #ccf;
-}
-td input:not(:focus) {
-    text-align: right;
-}
-
-

+ 0 - 17
frontend/public/index.html

@@ -1,17 +0,0 @@
-
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta content="width=device-width, initial-scale=1" name="viewport">
-    <link href="/css/site.css" rel="stylesheet" type="text/css">
-  </head>
-  <body>
-    <div id="app">
-      <h3>ClojureScript has not been compiled!</h3>
-      <p>please run <b>lein figwheel</b> in order to start the compiler</p>
-    </div>
-    <script src="/js/app.js" type="text/javascript"></script>
-    
-  </body>
-</html>

+ 16 - 0
frontend/resources/public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset='utf-8'>
+    <meta name="viewport" content="width=device-width,initial-scale=1">
+    <title>microtables-frontend</title>
+    <link rel="stylesheet" href="site.css">
+  </head>
+  <body>
+    <noscript>
+      microtables-frontend is a JavaScript app. Please enable JavaScript to continue.
+    </noscript>
+    <div id="app"></div>
+    <script src="js/compiled/app.js"></script>
+  </body>
+</html>

+ 88 - 0
frontend/resources/public/logo.svg

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
+<svg width="26cm" height="26cm" viewBox="92 135 503 502" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs/>
+  <g id="Background">
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="152" y="193" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="248.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="303.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="358.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.95" y="193.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="248.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.9" y="193.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.85" y="193.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="248.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="248.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="303.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="358.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="303.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="303.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="358.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="358.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.95" y="413.05" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="468.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.9" y="523.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.85" y="578.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.9" y="413.1" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="468.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.85" y="413.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.8" y="413.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="468.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="468.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.85" y="523.15" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.8" y="578.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #00afff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.8" y="523.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.75" y="523.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.75" y="578.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.7" y="578.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="151.8" y="138.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="206.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="261.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="316.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="371.75" y="138.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="426.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="481.7" y="138.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="536.65" y="138.35" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.8" y="193.2" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="248.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="303.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="358.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.75" y="413.25" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="468.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.7" y="523.3" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.65" y="578.35" width="55" height="55" rx="0" ry="0"/>
+    <rect style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 1; stroke-width: 6; stroke: #999999" x="95.6" y="138.4" width="55" height="55" rx="0" ry="0"/>
+  </g>
+</svg>

+ 219 - 0
frontend/resources/public/site.css

@@ -0,0 +1,219 @@
+body {
+  font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif;
+  max-width: 600px;
+  margin: 0;
+  -webkit-font-smoothing: antialiased;
+  font-size: 1.125em;
+  color: #333;
+  line-height: 1.5em;
+}
+
+h1, h2, h3 {
+  color: #000;
+}
+h1 {
+  font-size: 2.5em
+}
+
+h2 {
+  font-size: 2em
+}
+
+h3 {
+  font-size: 1.5em
+}
+
+a {
+  text-decoration: none;
+  color: #09f;
+}
+
+a:hover {
+  text-decoration: underline;
+}
+
+#main-table {
+    /*position: fixed;
+    top: 0;
+    left: 0;*/
+}
+
+table {
+    border-collapse: collapse;
+}
+
+td, th {
+    border: 1px solid black;
+    padding: 0;
+}
+th {
+    text-align: center;
+    min-width: 40px;
+}
+td {
+    text-align: right;
+    position: relative;
+}
+
+td input {
+    border: none;
+    padding: 5px;
+    width: 80px;
+}
+td input:hover {
+    background-color: #ccc;
+}
+td input:focus {
+    background-color: #ccf;
+}
+td input:not(:focus) {
+    text-align: right;
+}
+
+
+#controls {
+    --controls-width: 75px;
+    --controls-padding: 5px;
+    --controls-opener-width: 15px;
+}
+#controls > div, #controls > button {
+    position: fixed;
+    overflow: hidden;
+}
+/* TODO: design square logo for microtables */
+#main-logo {
+    background-color: lightblue;
+    z-index: 999;
+    left: 0;
+    bottom: 0;
+    padding: var(--controls-padding);
+    height: var(--controls-width);
+    width: var(--controls-width);
+    font-size: xxx-large;
+    font-weight: bold;
+    text-align: center;
+    border: 1px solid black;
+}
+#main-logo > img {
+    width: 100%;
+    height:100%;
+}
+#left-controls-button, #bottom-controls-button {
+    border: 1px solid black;
+    border-radius: 2px;
+    text-align: center;
+    font-weight: bold;
+    font-size: xx-large;
+    background-color: white;
+    cursor: pointer;
+    user-select: none;
+}
+#left-controls-button {
+    left: 0;
+    width: calc(var(--controls-width) + 2 * var(--controls-padding) - 2px);
+    height: var(--controls-opener-width);
+    z-index: 999;
+    bottom: calc(var(--controls-width) + 2 * var(--controls-padding));
+    line-height: 3px;
+    text-align: right;
+}
+#bottom-controls-button {
+    bottom: 0;
+    height: calc(var(--controls-width) + 2 * var(--controls-padding) - 2px);
+    width: var(--controls-opener-width);
+    z-index: 999;
+    left: calc(var(--controls-width) + 2 * var(--controls-padding));
+}
+#controls-left {
+    background-color: pink;
+    z-index: 998;
+    top: 0;
+    left: calc(-1 * var(--controls-width) - 2 * var(--controls-padding));
+    padding: 5px;
+    height: calc(100vh - var(--controls-padding));
+    width: var(--controls-width);
+    border-right: 1px solid black;
+}
+/*TODO: link left controls with position of the whole table*/
+#controls-left.open {
+    left: 0;
+}
+#controls-bottom {
+    background-color: lightblue;
+    z-index: 998;
+    left: 0;
+    bottom: calc(-1 * var(--controls-width) - 2 * var(--controls-padding));
+    height: var(--controls-width);
+    padding: var(--controls-padding);
+    padding-left: calc(var(--controls-width) + 3 * var(--controls-padding) + var(--controls-opener-width));
+    width: calc(100vw - var(--controls-width) - 4 * var(--controls-padding) - var(--controls-opener-width));
+    border-top: 1px solid black;
+}
+#controls-bottom.open {
+    bottom: 0;
+}
+
+
+.control-group {
+    float: left;
+    margin: 5px;
+    background-color: lightgreen;
+}
+.control-label {
+    font-size: x-small;
+}
+
+
+.smartborder {
+    width: calc(100% + 2px);
+    height: calc(100% + 2px);
+    border: 3px solid blue;
+    position: absolute;
+    left: -4px;
+    top: -4px;
+    background: none;
+    z-index: 999;
+    /*pointer-events: none;*/
+}
+
+td input:not(:focus) + .smartborder {
+    visibility: hidden;
+}
+
+
+.smartborder .button {
+    --button-size: 20px;
+    --half-button-size: 10px;
+    /*pointer-events: all;/* TODO check this (what is "initial"?) */
+    width: var(--button-size);
+    height: var(--button-size);
+    background-color: blue;
+    color: white;
+    border-radius: var(--half-button-size);
+    position: absolute;
+}
+.extendrange.button {
+    left: 100%;
+    top: 100%;
+}
+.fillrange.button {
+    top: 100%;
+    left: calc(50% - var(--half-button-size));
+}
+.moverange.button {
+    top: calc(50% - var(--half-button-size));
+    left: calc(0px - var(--button-size));
+}
+.emptyrange.button {
+    top: calc(0px - var(--button-size));
+    left: calc(100% - var(--button-size) - 5px);
+}
+.deleterange.button {
+    top: calc(0px - var(--button-size));
+    left: 100%;
+}
+.copyrange.button {
+    top: calc(0px - var(--button-size));
+    left: 0;
+}
+

+ 12 - 0
frontend/shadow-cljs.edn

@@ -0,0 +1,12 @@
+{:lein   true
+
+ :nrepl {:port 8777}
+
+ :builds {:app {:target          :browser
+                :output-dir      "resources/public/js/compiled"
+                :asset-path      "/js/compiled"
+                :modules         {:app {:init-fn microtables-frontend.core/init
+                                        :preloads [devtools.preload]}}
+                :devtools        {:http-root    "resources/public"
+                                  :http-port    8280
+                                  }}}}

+ 4 - 0
frontend/src/cljs/microtables_frontend/config.cljs

@@ -0,0 +1,4 @@
+(ns microtables-frontend.config)
+
+(def debug?
+  ^boolean goog.DEBUG)

+ 23 - 0
frontend/src/cljs/microtables_frontend/core.cljs

@@ -0,0 +1,23 @@
+(ns microtables-frontend.core
+  (:require
+   [reagent.core :as reagent]
+   [re-frame.core :as re-frame]
+   [microtables-frontend.events :as events]
+   [microtables-frontend.views :as views]
+   [microtables-frontend.config :as config]))
+
+
+
+(defn dev-setup []
+  (when config/debug?
+    (println "dev mode")))
+
+(defn ^:dev/after-load mount-root []
+  (re-frame/clear-subscription-cache!)
+  (reagent/render [views/main-panel]
+                  (.getElementById js/document "app")))
+
+(defn init []
+  (re-frame/dispatch-sync [::events/initialize-db])
+  (dev-setup)
+  (mount-root))

+ 23 - 0
frontend/src/cljs/microtables_frontend/db.cljs

@@ -0,0 +1,23 @@
+(ns microtables-frontend.db)
+
+(def default-db
+  {
+   :controls nil
+   ;TODO: add "start" and "end" corners as selection
+   :position {:cursor nil
+              :selection nil}
+   :table-data {"A" {1 {:value "59"}
+                     12 {:value "2405"}}
+                "B" {4 {:value "7893"}
+                     5 {:value "7863"}
+                     7 {:value "=C5 + D6"}
+                     8 {:value "=B7 * 2"}
+                     12 {:value "=C12"}}
+                "C" {7 {:value "=D1"}
+                     5 {:value "269"}}
+                "D" {6 {:value "4065"}
+                     10 {:value "8272"}
+                     11 {:value "2495"}}
+                "F" {2 {:value "8650"}
+                     7 {:value "5316"}}}})
+

+ 75 - 0
frontend/src/cljs/microtables_frontend/events.cljs

@@ -0,0 +1,75 @@
+(ns microtables-frontend.events
+  (:require
+   [re-frame.core :as re-frame]
+   [microtables-frontend.db :as db]
+   [microtables-frontend.utils :as utils]))
+
+
+
+
+(re-frame/reg-event-db
+ ::initialize-db
+ (fn [_ _]
+   (println "initializing db")
+   (-> db/default-db
+       (update-in [:table-data] #(utils/walk-modify-data % (fn [c r datum] (if (= (first (:value datum)) "=") (assoc datum :dirty true) datum))))
+       (update-in [:table-data] utils/create-all-references)
+       (update-in [:table-data] utils/create-all-back-references)
+       (update-in [:table-data] utils/evaluate-all))))
+
+
+(re-frame/reg-event-db
+  ::movement-enter-cell
+  (fn [db [_ c r]]
+    (println "::movement-enter-cell" c r)
+    (assoc-in db [:position :cursor] {:col c :row r})))
+
+
+
+(re-frame/reg-event-db
+  ::movement-leave-cell
+  (fn [db [_ c r]]
+    (println "::movement-leave-cell" c r)
+    (-> db
+        (assoc-in [:position :cursor] nil)
+        (assoc-in [:position :selection] nil)
+        (update-in [:table-data] #(utils/reset-references % c r))
+        (update-in [:table-data] #(utils/evaluate-from-cell % c r)))))
+
+
+(re-frame/reg-event-db
+  ::edit-cell-value
+  (fn [db [_ c r value]]
+    (println "::edit-cell-value" c r value)
+    (update-in db [:table-data] #(utils/change-datum-value % c r value))))
+
+; handle pressing enter (move to the next cell down)
+; tab is taken care of natively, and is good enough
+(re-frame/reg-event-fx
+  ::press-enter-in-cell
+  (fn [{:keys [db]} [_ c r]]
+    (let [max-row? (= (utils/highest-row (:table-data db)) r)
+          max-col? (= (utils/highest-col (:table-data db)) c)
+          new-col (if max-row?
+                    (if max-col?
+                      "A"
+                      (utils/next-letter c))
+                    c)
+          new-row (if max-row?
+                    1
+                    (inc r))]
+      (println "::press-enter-in-cell" c r)
+      {:focus-on-cell [new-col new-row]})))
+
+(re-frame/reg-fx
+  :focus-on-cell
+  (fn [[c r]]
+    (println "fx for :press-enter" c r)
+    (.focus (.getElementById js/document (str c r)))))
+
+
+(re-frame/reg-event-db
+ ::set-controls-state
+ (fn [db [_ new-state]]
+   (println "::set-controls-state" new-state)
+   (assoc-in db [:controls] new-state)))

+ 23 - 0
frontend/src/cljs/microtables_frontend/subs.cljs

@@ -0,0 +1,23 @@
+(ns microtables-frontend.subs
+  (:require
+   [re-frame.core :as re-frame]))
+
+(re-frame/reg-sub
+ ::controls-state
+ (fn [db]
+  (println "reporting state of controls")
+  (:controls db)))
+
+
+;TODO: insert other display mode data? ("value": formula (cursor), "display" (default): evaluated, "highlighted": in a selection (just a class?))
+(re-frame/reg-sub
+  ::table-data
+  (fn [db]
+    (println "returning table data")
+    (let [data (:table-data db)
+          cursor (get-in db [:position :cursor])]
+      (if cursor
+        (assoc-in data [(:col cursor) (:row cursor) :view] :value)
+        data))))
+
+

+ 357 - 0
frontend/src/cljs/microtables_frontend/utils.cljs

@@ -0,0 +1,357 @@
+(ns microtables-frontend.utils
+  (:require
+   ["mathjs" :as mathjs]))
+
+; to add an npm package to shadow-cljs:
+; https://clojureverse.org/t/guide-on-how-to-use-import-npm-modules-packages-in-clojurescript/2298
+; https://shadow-cljs.github.io/docs/UsersGuide.html#npm
+
+
+(defn highest-col
+  "Return the highest column (letter) for which there is a non-empty cell"
+  [data]
+  ; choose the "max" (alphabetical order) value among the longest keys
+  (apply max (val (apply max-key key (group-by #(.-length %) (keys data))))))
+
+(defn highest-row
+  "Return the highest row (number) for which there is a non-empty cell"
+  [data]
+  ; get all the row keys from all the column objects (and flatten), then pick the max
+  (apply max (flatten (map keys (vals data)))))
+
+
+
+(defn increment-letter-code [s]
+  (let [l (last s)]
+    (cond
+      (empty? s) [65]
+      (= l 90) (conj (increment-letter-code (subvec s 0 (dec (count s)))) 65)
+      :else (conj (subvec s 0 (dec (count s))) (inc l)))))
+
+
+(defn next-letter [lc]
+  (apply str (map char (increment-letter-code (mapv #(.charCodeAt % 0) lc)))))
+
+
+(def col-letters (iterate next-letter "A"))
+
+
+(defn order-two-cols
+  "Accepts two column names (letters) and returns them in order."
+  [col1 col2]
+  (cond
+    (> (.-length col1) (.-length col2)) [col2 col1]
+    (> (.-length col2) (.-length col1)) [col1 col2]
+    (= (max col1 col2) col1) [col2 col1]
+    :else [col1 col2]))
+
+; the order goes top to bottom, then left to right - that makes the most sense to me
+; I don't know why a different order would be important, or even in what situation order is important at all
+(defn parse-range
+  "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
+  [range-string]
+  (let [col1 (second (re-find #"\(\s*([A-Z]+)" range-string))
+        col2 (second (re-find #":\s*([A-Z]+)" range-string))
+        row1 (.parseInt js/window (second (re-find #"([0-9]+)\s*:" range-string)))
+        row2 (.parseInt js/window (second (re-find #"([0-9]+)\s*\)" range-string)))
+        [start-col end-col] (order-two-cols col1 col2)
+        start-row (min row1 row2)
+        end-row (max row1 row2)]
+    (for [col (take-while #(not= (next-letter end-col) %) (iterate next-letter start-col))
+             row (range start-row (inc end-row))]
+         {:col col :row row})))
+
+(def range->commalist
+  "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
+  (memoize (fn [range-string]
+            (let [cell-list (parse-range range-string)
+                  strings (map #(str (:col %) (:row %)) cell-list)]
+             (str "(" (clojure.string/join "," strings) ")")))))
+
+(def replace-ranges-in-expression
+  "Receives an expression string, and replaces all ranges in colon notation (\"A1:B2\") into a comma-separated list of cells (\"A1,A2,B1,B2\")."
+  (memoize (fn [expression]
+             (clojure.string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" parse-range))))
+
+(defn formula?
+  "Determines if a value is a fomula. If it is, it returns it (without the leading equals sign. If not, it returns nil."
+  [value]
+  (if (= (first value) "=")
+    (subs value 1)
+    nil))
+
+(def parse-variables (memoize (fn [expression]
+                                (as-> (js->clj (.parse mathjs (replace-ranges-in-expression expression))) $
+                                  (.filter $ #(true? (.-isSymbolNode %)))
+                                  (map #(.-name %) $)
+                                  (map #(.toUpperCase %) $)
+                                  (filter #(re-matches #"[A-Z]+[0-9]+" %) $)))))
+
+(def str->rc (memoize (fn [s]
+                        (let [c (re-find #"^[A-Z]+" s)
+                              r (.parseInt js/window (re-find #"[0-9]+$" s))]
+                          {:row r :col c}))))
+
+; leave in the :inbound references, since they probably have not have changed
+(defn add-references
+  "Parses the expression in the value of a datum, and adds refs as necessary"
+  [datum]
+  (let [formula (formula? (:value datum))]
+    (if formula
+      (let [vars (parse-variables formula)
+            refs (map str->rc vars)]
+        (-> datum
+            (assoc :refs refs)
+            (dissoc :error)))
+      (-> datum
+          (dissoc :refs)
+          (dissoc :display)
+          (dissoc :error)))))
+
+; the references in the data are a set of disconnected, doubly-linked trees
+;TODO: rather than denotify all, then re-notify all, maybe use a diff? maybe on small scales it's not worth it?
+(defn denotify-references
+  "Remove references in all cells formerly referenced by this cell"
+  [data origin refs]
+  (if (empty? refs)
+    data
+    (let [target (first refs)
+          de-notified (update-in data [(:col target) (:row target) :inbound] (partial filter #(not= % origin)))]
+      (recur de-notified origin (rest refs)))))
+(defn notify-references
+  "Update references in all cells referenced by this cell"
+  [data origin refs]
+  (if (empty? refs)
+    data
+    (let [target (first refs)
+          notified (update-in data [(:col target) (:row target) :inbound] conj origin)]
+      (recur notified origin (rest refs)))))
+(defn create-all-references
+  "Starting from a clean slate, add in all references. This wipes any references that may have been present."
+  [data]
+  (reduce-kv
+    (fn [columns c curr-column]
+      (assoc columns c (reduce-kv
+                         (fn [rows r datum]
+                           (assoc rows r (add-references (dissoc (dissoc datum :refs) :inbound))))
+                         {}
+                         curr-column)))
+    {}
+    data))
+
+;TODO: re-write create-all-references to use walk-modify-data instead
+(defn walk-modify-data
+  "Walks through the data map and updates each datum by applying f (a function accepting col, row, datum)."
+  [data f]
+  (reduce-kv
+    (fn [columns c curr-column]
+      (assoc columns c (reduce-kv
+                         (fn [rows r datum]
+                           (assoc rows r (f c r datum)))
+                         {}
+                         curr-column)))
+    {}
+    data))
+
+
+(defn walk-get-refs
+  "Walks through the data map and returns a list of :col/:row maps for each cell which satisfies the predicate (a function accepting col, row, datum)."
+  [data pred]
+  (reduce-kv (fn [l c column] (concat l (map (fn [[r _]] {:col c :row r}) (filter (fn [[r datum]] (pred c r datum)) column)))) '() data))
+
+; proposed alternative (the beginning of one) to walk-get-refs
+;(defn col-map? [m] (and (map? m) (every? #(and (string? %) (re-matches #"[A-Z]+" %)) (keys m))))
+;(defn row-map? [m] (and (map? m) (every? #(and (integer? %) (pos? %)) (keys m))))
+;(defn get-all-cells [data] (filter #(not (or (col-map? %) (row-map? %))) (tree-seq #(and (map? %) (or (col-map? %) (row-map? %))) vals data)))
+
+(defn create-all-back-references
+  "Assuming all references have been added, insert all back references."
+  [data]
+  (loop [data data
+         formulas (walk-get-refs data #(formula? (:value %3)))]
+    (if (empty? formulas)
+      data
+      (let [origin (first formulas)
+            refs (get-in data [(:col origin) (:row origin) :refs])
+            updated-one (notify-references data origin refs)]
+        (recur updated-one (rest formulas))))))
+
+(defn set-dirty-flags
+  "Sets the target cell to \"dirty\" and recursively repeat with its back-references all the way up. Returns the new data set."
+  ([data c r]
+   (set-dirty-flags data (list {:col c :row r})))
+  ([data queue]
+   (if (empty? queue)
+     data
+     (let [cur (first queue)
+           c (:col cur)
+           r (:row cur)
+           datum (get-in data [c r])]
+       (if (true? (:dirty datum))
+         (recur data (rest queue))
+         (let [new-data (assoc-in data [c r :dirty] true)
+               new-queue (concat (rest queue) (:inbound datum))]
+           (recur new-data new-queue)))))))
+
+
+(defn change-datum-value
+  "Modify the value of a datum in the table, and update all applicable references"
+  [data c r value]
+  (let [datum (get-in data [c r])
+        updated (assoc datum :value value)]
+    (-> data
+        (assoc-in [c r :value] value)
+        (set-dirty-flags c r))))
+
+(defn reset-references
+  "If there has been a change to which cells are referenced by this cell, then change the necessary back-references to this cell."
+  [data c r]
+  (let [datum (get-in data [c r])
+        parsed (add-references datum)]
+    (if (= (:refs datum) (:refs parsed))
+      data
+      (-> data
+          (assoc-in [c r] parsed)
+          (denotify-references {:col c :row r} (:refs datum))
+          (notify-references {:col c :row r} (:refs parsed))))))
+
+(defn remove-valueless-range-elements
+  "Remove nil values specifically from ranges (to solve issues with some functions like average)."
+  [variables var-list]
+  (println "remove-valueless-range-elements" variables var-list (first var-list))
+  (let [l (clojure.string/split (clojure.string/replace (first var-list) #"[()]" "") #",")
+        has-values (filter #(not (nil? (variables %))) l)]
+    (str "(" (clojure.string/join "," has-values) ")")))
+
+
+
+(defn preprocess-expression
+  "Handle range cases, rename certain functions (to work with math.js), prepare expression and variables for processing."
+  [expression variables]
+  (let [renamed-expression (clojure.string/replace expression #"\baverage\(" "mean(")
+        new-expression (clojure.string/replace renamed-expression #"\(([A-Z]+[0-9]+,)*[A-Z]+[0-9]+\)" (partial remove-valueless-range-elements variables))
+        new-variables (reduce-kv #(assoc %1 %2 (if (nil? %3) "0" %3)) {} variables)]
+    (println "PREPROCESS" {:expression new-expression :variables new-variables})
+    {:expression new-expression
+     :variables new-variables}))
+
+
+(def evaluate-expression
+  "Convert (via mathjs) an expression string to a final answer (also a string).  A map of variables must also be provided. If there is an error, it will return :calc-error."
+  (memoize (fn [expression variables]
+             (let [range-replaced (replace-ranges-in-expression expression)
+                   {ready-expression :expression ready-variables :variables} (preprocess-expression range-replaced variables)]
+               (try
+                 (.evaluate mathjs ready-expression (clj->js ready-variables))
+                 (catch js/Error e
+                   (println "mathjs evaluation error" (.-message e) e)
+                   :calc-error))))))
+
+;TODO: deal with lowercase cell references
+
+;TODO: memoize dynamically? probably not worth memoizing directly, and could take up too much memory over time
+;      https://stackoverflow.com/a/13123571/8172807
+(defn find-cycle
+  "Accepts the data and a datum, and peforms a depth-first search to find reference cycles, following back-references."
+  ([data c r] (find-cycle data c r #{}))
+  ([data c r ancest]
+   (let [datum (get-in data [c r])
+         current {:col c :row r}
+         this-and-above (conj ancest current)
+         inbound (:inbound datum)
+         found-repeat (not (empty? (clojure.set/intersection this-and-above (set inbound))))]
+     (if found-repeat
+       :cycle-error
+       (some #(find-cycle data (:col %) (:row %) this-and-above) inbound)))))
+
+
+(defn gather-variables-and-evaluate-cell
+  "Assumes that all the cell's immediate references have been resolved. Collects the final values from them, then evaluates the current cell's expression. Returns the new data map."
+  [data c r]
+  (let [datum (dissoc (dissoc (get-in data [c r]) :dirty) :display) ; get rid of the dirty flag right away (it must be included with the returned data to have effect)
+        refs (:refs datum)
+        value (:value datum)
+        formula (formula? value)
+        resolved-refs (map #(merge % (get-in data [(:col %) (:row %)])) refs)
+        evaluated-refs (map #(if (formula? (:value %)) (:display %) (:value %)) resolved-refs)
+        invalid-refs (some nil? resolved-refs)
+        dirty-refs (some :dirty resolved-refs)
+        error-refs (some #(= (:display %) :error) resolved-refs)
+        ;unevaluated-refs (some nil? evaluated-refs)
+        cycle-refs (some #(= (:display %) :cycle-error) resolved-refs)
+        disqualified? (or invalid-refs dirty-refs error-refs)]
+    (cond
+      (not formula) (assoc-in data [c r] datum)     ; if it's not a formula, then return as is (with the dirty flag removed)
+      cycle-refs (-> data                           ; if one of its references has a reference cycle, then this one is "poisoned" as well
+                     (assoc-in [c r] datum)
+                     (assoc-in [c r :display] :cycle-error))
+      ;unevaluated-refs (assoc-in data [c r :display] :insufficient-data) ; do not un-mark as "dirty", since it has not been evaluated yet
+      disqualified? (-> data                        ; some other error is present
+                        (assoc-in [c r] datum)
+                        (assoc-in [c r :display] :error))
+      (empty? refs) (-> data
+                        (assoc-in [c r] datum)
+                        (assoc-in [c r :display] (evaluate-expression (subs value 1) {})))
+      :else (let [variables (zipmap (map #(str (:col %) (:row %)) refs) evaluated-refs)
+                  evaluated-value (evaluate-expression (subs value 1) variables)
+                  new-datum (assoc datum :display evaluated-value)]
+              (assoc-in data [c r] new-datum)))))
+
+
+; THE NEW EVALUATE FUNCTION
+; - check for cycles in the back references, starting from the target cell (if any, use another function to mark it and its back references with :cycle-error and remove :dirty)
+; - if any of the forward references are dirty, mark the cell (and recurse up) with an error (and set a TODO to think about this further)
+; - evaluate (using forward references if necessary)
+; - add all back-references to the queue
+; - recurse
+; - TODO: consider initialization case
+; - TODO: consider multiple cells modified simultaneously
+(defn evaluate-from-cell
+  "Evaluate the final value of a cell, and recursively re-evaluate all the cells that reference it."
+  [data c r]
+  (let [cycles? (find-cycle data c r)
+        new-data (if cycles?
+                   (-> data                                           ; if there are cycles, mark :cycle-error and remove :dirty (rathan than evaluate) - still need to recurse up the tree to mark dependents with :cycle-error
+                       (update-in [c r] dissoc :dirty)
+                       (assoc-in [c r :display] :cycle-error))
+                   (gather-variables-and-evaluate-cell data c r))]    ; if there are no cycles, evaluate the cell
+    (loop [data new-data
+           queue (get-in new-data [c r :inbound])]
+      (if (empty? queue)
+        data                                                          ; if the queue is empty, we're done
+        (let [current (first queue)
+              cc (:col current)
+              cr (:row current)
+              dirty? (get-in data [cc cr :dirty])
+              re-evaluated-data (if dirty?
+                                  (gather-variables-and-evaluate-cell data cc cr)
+                                  data)
+              sufficient? (not= (get-in re-evaluated-data [cc cr :display]) :insufficient-data)
+              new-queue (if dirty?
+                          (if sufficient?
+                            (concat (rest queue) (get-in re-evaluated-data [cc cr :inbound]))   ; if all is well, then add the back-references onto the queue
+                            (concat (rest queue) (list current)))                               ; if the current cell's dependencies are not satisfied, re-add to the end of the queue
+                          (rest queue))]                                                        ; if the current cell is not marked as dirty, then it has already been processed
+          (recur re-evaluated-data new-queue))))))
+
+;TODO: does this need a cycle check?
+(defn evaluate-all
+  "Evaluates all cells marked as \"dirty\". Generally reserved for the initialization."
+  ([data]
+   (evaluate-all data (walk-get-refs data #(:dirty %3))))
+  ([data queue]
+   (if (empty? queue)
+     data
+     (let [cur (first queue)
+           cc (:col cur)
+           cr (:row cur)
+           dirty? (get-in data [cc cr :dirty])]
+       (if dirty?
+         (let [evaluated (evaluate-from-cell data (:col cur) (:row cur))
+               result (get-in evaluated [cc cr :display])]
+           (if (= result :insufficient-data)
+             (recur data (concat (rest queue) (list cur)))
+             (recur evaluated (rest queue))))
+         (recur data (rest queue)))))))
+
+

+ 108 - 0
frontend/src/cljs/microtables_frontend/views.cljs

@@ -0,0 +1,108 @@
+(ns microtables-frontend.views
+  (:require
+   [re-frame.core :as re-frame]
+   [microtables-frontend.subs :as subs]
+   [microtables-frontend.events :as events]
+   [microtables-frontend.utils :as utils]))
+
+
+;; TABLE COMPONENTS
+
+(defn cell [c r data]
+  (let [datum (get-in data [c r])]
+    ^{:key (str c r)} [:td
+                       [:input {:id (str c r)
+                                :value (if (= (get datum :view nil) :value)
+                                         (get datum :value "")
+                                         (get datum :error (get datum :display (get datum :value ""))));TODO: add "highlight" display mode (possibly just a css class)
+                                :on-change #(re-frame/dispatch [::events/edit-cell-value c r (.. % -target -value)])
+                                :on-focus #(re-frame/dispatch [::events/movement-enter-cell c r])
+                                :on-blur #(re-frame/dispatch [::events/movement-leave-cell c r])
+                                :on-keyPress #(when (= (.. % -which) 13)
+                                                (re-frame/dispatch [::events/press-enter-in-cell c r]))}]]))
+
+(defn row [r cols data]
+  ^{:key (str "row-" r)} [:tr
+                          (cons
+                           ^{:key (str "row-head-" r)} [:th (str r)]
+                           (map #(cell % r data) cols))])
+
+(defn header-row [cols]
+  ^{:key "header"} [:tr
+                    (cons
+                      ^{:key "corner"} [:th]
+                      (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))])
+
+(defn sheet [data]
+  [:table
+   {:id "main-table"}
+   [:tbody
+    ;TODO: figure out appropriate starting values for maxrow and maxcol (maybe keep them intentionally small)
+    ;TODO: figure out movement (maybe allow scroll overflow)
+    (let [maxrow 50;(utils/highest-row data)
+          maxcol "Z";(utils/highest-col data)
+          cols (take-while (partial not= (utils/next-letter maxcol)) utils/col-letters)]
+      (cons
+        (header-row cols)
+        (map #(row % cols data) (range 1 (inc maxrow)))))]])
+
+
+(defn extend-range
+  []
+  [:div {:class "control-group"}
+   [:div {:class "control-label"}
+    "Extend Range"]
+   [:button "-cols"]
+   [:input]
+   [:button]])
+
+(defn controls []
+  [:div
+   ;[extend-range]
+   ;extend range (number field, then shrink/left/up and expand/right/down buttons on either side)
+   ;fill range (single tap, single column/row fill? number field and button?)
+   ;move range (designate new "top-left corner" cell for range, figure out overwriting destination cells)
+   ;empty range (delete contents, re-evaluate)
+   ;delete range (figure out how to move cells over)
+   ;copy range (figure out clipboard)
+   [:div
+    {:class "control-group"}
+    [:a ;TODO: consider making the "About" info an overlay rather than a link
+     {:href "/about.html"
+      :target "_blank"}
+     "About"]]])
+
+(defn control-panel [state]
+  [:div
+   {:id "controls"}
+   [:div
+    ;TODO: link left controls with position of the whole table
+    {:id "controls-left"
+     :class (if (= state :left) "open" "")}
+    [controls]]
+   [:div
+    {:id "controls-bottom"
+     :class (if (= state :bottom) "open" "")}
+    [controls]]
+   ;←↑→↓
+   [:div
+    {:id "left-controls-button"
+     :on-click #(re-frame/dispatch [::events/set-controls-state (if (= state :left) nil :left)])}
+    (if (= state :left) "←" "→")]
+   [:div
+    {:id "bottom-controls-button"
+     :on-click #(re-frame/dispatch [::events/set-controls-state (if (= state :bottom) nil :bottom)])}
+    (if (= state :bottom) "↓" "↑")]
+   [:div
+    {:id "main-logo"
+     :title "Microtables"}
+    [:img {:src "logo.svg"}]]])
+
+(defn main-panel []
+  (let [data (re-frame/subscribe [::subs/table-data])
+        controls-state (re-frame/subscribe [::subs/controls-state])]
+    [:div
+     [sheet @data]
+     [control-panel @controls-state]]))
+
+

+ 0 - 27
frontend/src/js/index.js

@@ -1,27 +0,0 @@
-
-// to include an npm module/package into clojurescript:
-// https://figwheel.org/docs/npm.html
-
-// to reduce the bundle size:
-// https://mathjs.org/docs/custom_bundling.html
-
-import { parse, evaluate } from 'mathjs';
-
-function getVariables(expression) {
-    if( !expression || typeof expression !== 'string' ) {
-        console.log('not correct input', expression, typeof expression);
-        return [];
-    }
-    try {
-        const expressionObject = parse(expression);
-        console.log('now returning', expressionObject);
-        return expressionObject.filter(x => x.isSymbolNode).map(x => x.name);
-    }
-    catch(e) {
-        console.error('error in parsing or filtering for variables', e.message || e);
-        return [];
-    }
-}
-
-window.mathjs = {parse, evaluate, getVariables};
-

+ 0 - 185
frontend/src/microtables_frontend/core.cljs

@@ -1,185 +0,0 @@
-(ns microtables-frontend.core
-    (:require
-      [reagent.core :as r]
-      ["mathjs" :as mathjs]))
-
-; to generate random values
-;for(let i = 0, s = new Set(); i < 10; i++){ let r = Math.floor(Math.random() * 15)+1, c = a[Math.floor(Math.random() * a.length)], k = `${c}${r}`; if(s.has(k)){ i--; continue; } s.add(k); v.push(`{:row ${r} :col "${c}" :value "${Math.floor(Math.random() * 10000)}"}`); }
-(def sample-data [{:row 1 :col "A" :value "59" :view :display}
-                  {:row 5 :col "C" :value "269" :view :display}
-                  {:row 4 :col "B" :value "7893" :view :display}
-                  {:row 2 :col "F" :value "8650" :view :display}
-                  {:row 6 :col "D" :value "4065" :view :display}
-                  {:row 7 :col "F" :value "5316" :view :display}
-                  {:row 12 :col "A" :value "2405" :view :display}
-                  {:row 5 :col "B" :value "7863" :view :display}
-                  {:row 9 :col "E" :value "3144" :view :display}
-                  {:row 10 :col "D" :value "8272" :view :display}
-                  {:row 11 :col "D" :value "2495" :view :display}
-                  {:row 15 :col "E" :value "8968" :view :display}
-                  {:row 7 :col "B" :value "=C5 + D6" :view :display}
-                  {:row 8 :col "B" :value "=B7 * 2" :view :display}
-                  {:row 7 :col "C" :value "=D1" :view :display}
-                  {:row 12 :col "B" :value "=C12" :view :display}])
-
-(defonce data-atom (r/atom sample-data))
-
-(defn highest [dir data] (apply max (map dir data)))
-
-; COLUMN NAMES
-(defn upgrade-letter-code [s]
-  (let [l (last s)]
-    (cond
-      (empty? s) [65]
-      (= l 90) (conj (upgrade-letter-code (subvec s 0 (dec (count s)))) 65)
-      :else (conj (subvec s 0 (dec (count s))) (inc l)))))
-(defn next-letter [lc]
-  (apply str (map char (upgrade-letter-code (mapv #(.charCodeAt % 0) lc)))))
-(def col-letters (iterate next-letter "A"))
-
-
-; CHANGE VALUE FUNCTIONS
-(defn update-value [c r existing-datum value]
-  (if (nil? existing-datum)
-    (swap! data-atom conj {:row r :col c :value value :dirty true})
-    (swap! data-atom (partial map #(if (and (= r (:row %)) (= c (:col %))) (assoc (assoc % :dirty true) :value value) %)))))
-
-;TODO: consider changing this for a single "view" atom which points to zero or one cells, which will determine whether to show the formula or evaluation
-(defn toggle-display [data c r view-mode]
-  (println (str "  toggling " c r " to " view-mode))
-  (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :view view-mode) %) data))
-
-; CALCULATION / FORMULA EVALUATION FUNCTIONS
-
-(def parse-variables (memoize (fn [expression]
-                                (js->clj (.getVariables mathjs expression)))))
-(def evaluate-expression (memoize (fn [expression variables]
-                                    (.evaluate mathjs expression (clj->js variables)))))
-
-(def str->rc (memoize (fn [s]
-                        (let [c (re-find #"^[A-Z]+" s)
-                              r (.parseInt js/window (re-find #"[0-9]+$" s))]
-                          {:row r :col c}))))
-
-;TODO: deal with lowercase cell references
-(defn find-cell [data c r]
-  (some #(if (and (= (:col %) c) (= (:row %) r)) %) data))
-(defn find-ref [data cell-ref]
-  (some (fn [{:keys [row col] :as datum}] (if (and (= row (:row cell-ref)) (= col (:col cell-ref))) datum)) data))
-(defn copy-display-values [data display-values]
-  (let [original (map #(dissoc % :dirty) data)
-        removed (map #(-> % (dissoc :found) (dissoc :inputs) (dissoc :dirty)) display-values)]
-    (into original removed)))
-
-;TODO: memoize dynamically? probably not worth memoizing directly, and could take up too much memory over time
-;      https://stackoverflow.com/a/13123571/8172807
-(defn find-cycle
-  ([data datum] (find-cycle data datum #{}))
-  ([data datum ances]
-   (let [cur {:row (:row datum) :col (:col datum)}
-         this-and-above (conj ances cur)
-         refs (:refs datum)
-         found (not (empty? (clojure.set/intersection this-and-above (set refs))))]
-     (if found
-       :cycle-error
-       (some (fn [cell]
-               (find-cycle data (find-ref data cell) this-and-above)) refs)))))
-
-(defn find-val [data c r]
-  (let [l (find-cell data c r)
-        v (get l :display (get l :value))
-        formula? (and (string? v) (= (first v) "="))]
-    (cond
-      (nil? v) 0
-      ;(contains? l :error) :ref-error
-      formula? :not-yet
-      :else v)))
-
-
-
-;TODO: figure out how to re-evaluate only when the cell modified affects other cells
-(defn re-evaluate [data]
-  (let [{has-formula true original-values false} (group-by #(= (first (:value %)) "=") data)
-        found-cycles (map #(let [found (find-cycle data %)] (if found (assoc % :error found) %)) has-formula)
-        {eligible true ineligible false} (group-by #(not (contains? %  :error)) found-cycles)]
-    (loop [values (into original-values ineligible) mapped-cell-keys eligible]
-      (let [search-values (map (fn [datum] (assoc datum :found (map #(find-val (concat values mapped-cell-keys) (:col %) (:row %)) (:refs datum)))) mapped-cell-keys)
-            {not-ready true ready nil} (group-by (fn [datum] (some #(= :not-yet %) (:found datum))) search-values)
-            prepped-for-eval (map (fn [datum] (assoc datum :inputs (apply hash-map (interleave (:vars datum) (:found datum))))) ready)
-            evaluated (map (fn [datum] (assoc datum :display (evaluate-expression (subs (:value datum) 1) (:inputs datum)))) prepped-for-eval)
-            updated-values (copy-display-values values evaluated)]
-        (if (nil? not-ready)
-          updated-values
-          (recur updated-values not-ready))))))
-
-(defn add-parsed-variables [datum]
-  (if (= (first (:value datum)) "=")
-    (let [vars (parse-variables (subs (:value datum) 1))
-          refs (map str->rc vars)]
-      (-> datum (assoc :vars vars) (assoc :refs refs) (dissoc :error)))
-    (-> datum (dissoc :vars) (dissoc :refs) (dissoc :display) (dissoc :error))))
-
-(defn add-parsed-variables-to-specific-datum
-  "Parse variables from the value of a datum and add in :vars and :refs (for swap! data-atom).
-  If the value does not contain a fomula, remove any :vars and :refs that may have been there."
-  [c r data] (map #(if (and (= (:col %) c) (= (:row %) r))
-                     (add-parsed-variables %)
-                     %) data))
-
-(defn on-enter-cell [c r e]
-  (println (str "entering cell " c r))
-  (swap! data-atom #(toggle-display % c r :value)))
-(defn on-leave-cell [c r e]
-  (println (str "leaving cell " c r))
-  (swap! data-atom #(as-> % data
-                      (toggle-display data c r :display)
-                      (add-parsed-variables-to-specific-datum c r data)
-                      (re-evaluate data))))
-
-
-;; -------------------------
-;; Views
-
-(defn cell [c r data]
-  (let [datum (some #(if (and (= c (:col %)) (= r (:row %))) %) data)]
-    ^{:key (str c r)} [:td [:input {:value (if (= (get datum :view nil) :value)
-                                             (get datum :value "")
-                                             (get datum :error (get datum :display (get datum :value ""))))
-                                    :on-change #(update-value c r datum (.. % -target -value))
-                                    :on-blur (partial on-leave-cell c r)
-                                    :on-focus (partial on-enter-cell c r)}]]))
-(defn row [r cols data]
-  ^{:key (str "row-" r)} [:tr
-                          (cons
-                            ^{:key (str "row-head-" r)} [:th (str r)]
-                            (map #(cell % r data) cols))])
-(defn header-row [cols]
-  ^{:key "header"} [:tr
-   (cons
-     ^{:key "corner"} [:th]
-     (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))])
-
-(defn sheet [data]
-  [:table [:tbody
-           (let [maxrow (highest :row data)
-                 cols (take-while (partial not= (next-letter (highest :col data))) col-letters)]
-             (cons
-               (header-row cols)
-               (map #(row % cols data) (range 1 (inc maxrow)))))]])
-
-(defn app []
-  [:div
-   [:h3 "Microtables"]
-   [sheet @data-atom]])
-
-;; -------------------------
-;; Initialize app
-
-(swap! data-atom #(->> % (map add-parsed-variables) (re-evaluate))) ; evalutate any formulas the first time
-
-(defn mount-root []
-  (r/render [app] (.getElementById js/document "app")))
-
-(defn init! []
-  (mount-root))
-

+ 0 - 6
frontend/webpack.config.js

@@ -1,6 +0,0 @@
-module.exports = {
-    entry: './src/js/index.js',
-    output: {
-        filename: 'index.bundle.js',
-    },
-};

File diff suppressed because it is too large
+ 0 - 2978
frontend/yarn.lock


BIN
microtables.png