diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..643c59e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/*.log +/target +/*-init.clj +/resources/public/js/compiled +out diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..5f25843 --- /dev/null +++ b/project.clj @@ -0,0 +1,50 @@ +(defproject swingometer "0.1.0-SNAPSHOT" + :dependencies [[org.clojure/clojure "1.8.0"] + [org.clojure/clojurescript "1.9.229"] + [reagent "0.6.0"] + [re-frame "0.9.4"] + [re-com "2.0.0"]] + + :plugins [[lein-cljsbuild "1.1.4"]] + + :min-lein-version "2.5.3" + + :source-paths ["src/clj"] + + :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] + + :figwheel {:css-dirs ["resources/public/css"]} + + :profiles + {:dev + {:dependencies [[binaryage/devtools "0.8.2"]] + + :plugins [[lein-figwheel "0.5.9"]] + }} + + :cljsbuild + {:builds + [{:id "dev" + :source-paths ["src/cljs"] + :figwheel {:on-jsload "swingometer.core/mount-root"} + :compiler {:main swingometer.core + :output-to "resources/public/js/compiled/app.js" + :output-dir "resources/public/js/compiled/out" + :asset-path "js/compiled/out" + :source-map-timestamp true + :preloads [devtools.preload] + :external-config {:devtools/config {:features-to-install :all}} + }} + + {:id "min" + :source-paths ["src/cljs"] + :compiler {:main swingometer.core + :output-to "resources/public/js/compiled/app.js" + :optimizations :advanced + :closure-defines {goog.DEBUG false} + :pretty-print false}} + + + ]} + + ) diff --git a/resources/public/css/re-com.css b/resources/public/css/re-com.css new file mode 100644 index 0000000..026254e --- /dev/null +++ b/resources/public/css/re-com.css @@ -0,0 +1,1352 @@ +/*---------------------------------------------------------------------------------------- + re-com.css + + This file is required for styling the re-com UI library components. + To use the library, bootstrap.css is also required, which is best obtained from a CDN. + So, place the following lines in the section of the html file: + + + + + The following is required for constraining an application to the + height of the browser window and setting some global defaults like font... +----------------------------------------------------------------------------------------*/ + +/* The following style addresses: http://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox */ +* { + min-height: 0px; + min-width: 0px; +} + +html, body { + font-family: Segoe UI, Roboto, sans-serif; + font-size: 14px; + font-weight: 400; + color: rgba(68, 68, 68, 1.0); + letter-spacing: 0.002em; + height: 100%; + margin: 0px; + padding: 0px; +} + +/*---------------------------------------------------------------------------------------- + Bootstrap overrides after upgrading to 3.3.5 +----------------------------------------------------------------------------------------*/ + +.popover, .tooltip { + font-family: Segoe UI, Roboto, sans-serif; +} + +.popover-title { + font-weight: normal; +} + +.form-control-feedback { + pointer-events: initial; +} + + +/*---------------------------------------------------------------------------------------- + The section immediately below is required for the drop-down components and comes from + the bootstrap-chosen library: + + https://github.com/alxlit/bootstrap-chosen + License: MIT. + + That library provides it's css as a .less file. + To compile the less file into the css below, use the following steps from + a command prompt (you'll need git and the lessc compiler (http://lesscss.org): + + git clone https://github.com/alxlit/bootstrap-chosen + + cd bootstrap-chosen + + git clone --depth=1 https://github.com/twbs/bootstrap + + Now edit bootstrap-chosen.less and add the following two lines before the first @import: + @import "bootstrap/less/variables.less"; + @import "bootstrap/less/mixins.less"; + + lessc bootstrap-chosen.less > bootstrap-chosen.css + + Finally, replace the section below with the contents of bootstrap-chosen.css. + + START OF BOOTSTRAP-CHOSEN SECTION... +----------------------------------------------------------------------------------------*/ + +.chosen-select { + width: 100%; +} +.chosen-select-deselect { + width: 100%; +} +.chosen-container { + display: inline-block; + font-size: 14px; + position: relative; + vertical-align: middle; +} +.chosen-container .chosen-drop { + background: #ffffff; + border: 1px solid #cccccc; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: 0 8px 8px rgba(0, 0, 0, .25); + box-shadow: 0 8px 8px rgba(0, 0, 0, .25); + margin-top: -1px; + position: absolute; + top: 100%; + left: -9000px; + z-index: 1060; +} +.chosen-container.chosen-with-drop .chosen-drop { + left: 0; + right: 0; +} +.chosen-container .chosen-results { + color: #555555; + margin: 0 4px 4px 0; + max-height: 240px; + padding: 0 0 0 4px; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.chosen-container .chosen-results li { + display: none; + line-height: 1.42857143; + list-style: none; + margin: 0; + padding: 5px 6px; +} +.chosen-container .chosen-results li em { + background: #feffde; + font-style: normal; +} +.chosen-container .chosen-results li.group-result { + display: list-item; + cursor: default; + color: #999; + font-weight: bold; +} +.chosen-container .chosen-results li.group-option { + padding-left: 15px; +} +.chosen-container .chosen-results li.active-result { + cursor: pointer; + display: list-item; +} +.chosen-container .chosen-results li.highlighted { + background-color: #428bca; + background-image: none; + color: white; +} +.chosen-container .chosen-results li.highlighted em { + background: transparent; +} +.chosen-container .chosen-results li.disabled-result { + display: list-item; + color: #777777; +} +.chosen-container .chosen-results .no-results { + background: #eeeeee; + display: list-item; +} +.chosen-container .chosen-results .error { + text-align: center; + background: #f2dede; + display: list-item; +} +.chosen-container .chosen-results .loading { + text-align: center; + color: #ccc; + display: list-item; +} +.chosen-container .chosen-results-scroll { + background: white; + margin: 0 4px; + position: absolute; + text-align: center; + width: 321px; + z-index: 1; +} +.chosen-container .chosen-results-scroll span { + display: inline-block; + height: 1.42857143; + text-indent: -5000px; + width: 9px; +} +.chosen-container .chosen-results-scroll-down { + bottom: 0; +} +.chosen-container .chosen-results-scroll-down span { + background: url("chosen-sprite.png") no-repeat -4px -3px; +} +.chosen-container .chosen-results-scroll-up span { + background: url("chosen-sprite.png") no-repeat -22px -3px; +} +.chosen-container-single .chosen-single { + background-color: #ffffff; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + border: 1px solid #cccccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + color: #555555; + display: block; + height: 34px; + overflow: hidden; + line-height: 34px; + padding: 0 0 0 8px; + position: relative; + text-decoration: none; + white-space: nowrap; +} +.chosen-container-single .chosen-single span { + display: block; + margin-right: 26px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.chosen-container-single .chosen-single abbr { + background: url("chosen-sprite.png") right top no-repeat; + display: block; + font-size: 1px; + height: 10px; + position: absolute; + right: 26px; + top: 12px; + width: 12px; +} +.chosen-container-single .chosen-single abbr:hover { + background-position: right -11px; +} +.chosen-container-single .chosen-single.chosen-disabled .chosen-single abbr:hover { + background-position: right 2px; +} +.chosen-container-single .chosen-single div { + display: block; + height: 100%; + position: absolute; + top: 0; + right: 0; + width: 18px; +} +.chosen-container-single .chosen-single div b { + background: url("chosen-sprite.png") no-repeat 0 7px; + display: block; + height: 100%; + width: 100%; +} +.chosen-container-single .chosen-default { + color: #777777; +} +.chosen-container-single .chosen-search { + margin: 0; + padding: 3px 4px; + position: relative; + white-space: nowrap; + z-index: 1000; +} +.chosen-container-single .chosen-search input[type="text"] { + background: url("chosen-sprite.png") no-repeat 100% -20px, #ffffff; + border: 1px solid #cccccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + margin: 1px 0; + padding: 4px 20px 4px 4px; + width: 100%; +} +.chosen-container-single .chosen-drop { + margin-top: -1px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} +.chosen-container-single-nosearch .chosen-search input { + position: absolute; + left: -9000px; +} +.chosen-container-multi .chosen-choices { + background-color: #ffffff; + border: 1px solid #cccccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + cursor: text; + height: auto !important; + height: 1%; + margin: 0; + overflow: hidden; + padding: 0; + position: relative; +} +.chosen-container-multi .chosen-choices li { + float: left; + list-style: none; +} +.chosen-container-multi .chosen-choices .search-field { + margin: 0; + padding: 0; + white-space: nowrap; +} +.chosen-container-multi .chosen-choices .search-field input[type="text"] { + background: transparent !important; + border: 0 !important; + -webkit-box-shadow: none; + box-shadow: none; + color: #555555; + height: 32px; + margin: 0; + padding: 4px; + outline: 0; +} +.chosen-container-multi .chosen-choices .search-field .default { + color: #999; +} +.chosen-container-multi .chosen-choices .search-choice { + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + background-color: #eeeeee; + border: 1px solid #cccccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + background-image: -webkit-linear-gradient(top, #ffffff 0%, #eeeeee 100%); + background-image: -o-linear-gradient(top, #ffffff 0%, #eeeeee 100%); + background-image: linear-gradient(to bottom, #ffffff 0%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffeeeeee', GradientType=0); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + color: #333333; + cursor: default; + line-height: 13px; + margin: 6px 0 3px 5px; + padding: 3px 20px 3px 5px; + position: relative; +} +.chosen-container-multi .chosen-choices .search-choice .search-choice-close { + background: url("chosen-sprite.png") right top no-repeat; + display: block; + font-size: 1px; + height: 10px; + position: absolute; + right: 4px; + top: 5px; + width: 12px; + cursor: pointer; +} +.chosen-container-multi .chosen-choices .search-choice .search-choice-close:hover { + background-position: right -11px; +} +.chosen-container-multi .chosen-choices .search-choice-focus { + background: #d4d4d4; +} +.chosen-container-multi .chosen-choices .search-choice-focus .search-choice-close { + background-position: right -11px; +} +.chosen-container-multi .chosen-results { + margin: 0 0 0 0; + padding: 0; +} +.chosen-container-multi .chosen-drop .result-selected { + display: none; +} +.chosen-container-active .chosen-single { + border: 1px solid #66afe9; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + -webkit-transition: border linear .2s, box-shadow linear .2s; + -o-transition: border linear .2s, box-shadow linear .2s; + transition: border linear .2s, box-shadow linear .2s; +} +.chosen-container-active.chosen-with-drop .chosen-single { + background-color: #ffffff; + border: 1px solid #66afe9; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + -webkit-transition: border linear .2s, box-shadow linear .2s; + -o-transition: border linear .2s, box-shadow linear .2s; + transition: border linear .2s, box-shadow linear .2s; +} +.chosen-container-active.chosen-with-drop .chosen-single div { + background: transparent; + border-left: none; +} +.chosen-container-active.chosen-with-drop .chosen-single div b { + background-position: -18px 7px; +} +.chosen-container-active .chosen-choices { + border: 1px solid #66afe9; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(82, 168, 236, .6); + -webkit-transition: border linear .2s, box-shadow linear .2s; + -o-transition: border linear .2s, box-shadow linear .2s; + transition: border linear .2s, box-shadow linear .2s; +} +.chosen-container-active .chosen-choices .search-field input[type="text"] { + color: #111 !important; +} +.chosen-container-active.chosen-with-drop .chosen-choices { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.chosen-disabled { + cursor: default; + opacity: 0.5 !important; +} +.chosen-disabled .chosen-single { + cursor: default; +} +.chosen-disabled .chosen-choices .search-choice .search-choice-close { + cursor: default; +} +.chosen-rtl { + text-align: right; +} +.chosen-rtl .chosen-single { + padding: 0 8px 0 0; + overflow: visible; +} +.chosen-rtl .chosen-single span { + margin-left: 26px; + margin-right: 0; + direction: rtl; +} +.chosen-rtl .chosen-single div { + left: 7px; + right: auto; +} +.chosen-rtl .chosen-single abbr { + left: 26px; + right: auto; +} +.chosen-rtl .chosen-choices .search-field input[type="text"] { + direction: rtl; +} +.chosen-rtl .chosen-choices li { + float: right; +} +.chosen-rtl .chosen-choices .search-choice { + margin: 6px 5px 3px 0; + padding: 3px 5px 3px 19px; +} +.chosen-rtl .chosen-choices .search-choice .search-choice-close { + background-position: right top; + left: 4px; + right: auto; +} +.chosen-rtl.chosen-container-single .chosen-results { + margin: 0 0 4px 4px; + padding: 0 4px 0 0; +} +.chosen-rtl .chosen-results .group-option { + padding-left: 0; + padding-right: 15px; +} +.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div { + border-right: none; +} +.chosen-rtl .chosen-search input[type="text"] { + background: url("chosen-sprite.png") no-repeat -28px -20px, #ffffff; + direction: rtl; + padding: 4px 5px 4px 20px; +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) { + .chosen-rtl .chosen-search input[type="text"], + .chosen-container-single .chosen-single abbr, + .chosen-container-single .chosen-single div b, + .chosen-container-single .chosen-search input[type="text"], + .chosen-container-multi .chosen-choices .search-choice .search-choice-close, + .chosen-container .chosen-results-scroll-down span, + .chosen-container .chosen-results-scroll-up span { + background-image: url("chosen-sprite@2x.png") !important; + background-size: 52px 37px !important; + background-repeat: no-repeat !important; + } +} + +/*---------------------------------------------------------------------------------------- + ...END OF BOOTSTRAP-CHOSEN SECTION + + bootstrap-chosen additions +----------------------------------------------------------------------------------------*/ + +/* Mouse hover color in drop-downs */ +.chosen-container .chosen-results li.mouseover { + background-color: #eeeeee; + background-image: none; +} + +/*---------------------------------------------------------------------------------------- + Stylesheet for re-com.date Date Picker variants inline-picker & dropdown-picker + Day8 variation loosely based on: + Copyright 2013 Dan Grossman ( http://www.dangrossman.info ) + Licensed under the Apache License v2.0 + http://www.apache.org/licenses/LICENSE-2.0 + Built for http://www.improvely.com + http://eternicode.github.io/bootstrap-datepicker + + START OF DATE PICKER SECTION... +----------------------------------------------------------------------------------------*/ + +.datepicker.single .calendar { + float: none; +} + +.datepicker .calendar { + display: none; + max-width: 200px; +} + +.datepicker .calendar.single .calendar-date { + border: none; +} + +.datepicker .calendar th, .datepicker .calendar td { + white-space: nowrap; + text-align: center; + min-width: 32px; +} + +.datepicker .calendar-date { + border: 1px solid #ddd; + padding: 4px; + border-radius: 4px; + background: #fff; +} + +.datepicker .calendar-time { + text-align: center; + margin: 8px auto 0 auto; + line-height: 30px; +} + +.datepicker { + position: absolute; + background: #fff; + top: 100px; + left: 20px; + padding: 10px 10px 5px; + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + line-height: 16px; + border-radius: 4px; +} + +.datepicker table { + width: 100%; + margin: 0; + border-collapse: separate; +} + +.datepicker td, .datepicker th { + text-align: center; + width: 27px; + height: 26px; + max-width: 27px; + max-height: 26px; + min-width: 27px; + min-height: 26px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 4px; + cursor: default; + white-space: nowrap; + font-weight: normal; +} + +.datepicker td.off { + padding: 4px; + color: #999; +} + +.datepicker td.disabled { + color: #999; +} + +.datepicker th.disabled { + color: #999; +} + +.datepicker td.available:hover, .datepicker th.available:hover { + background: #eee; + cursor: pointer; +} + +.datepicker td.in-range { + background: #ebf4f8; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.datepicker td.start-date { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.datepicker td.end-date { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.datepicker td.start-date.end-date { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.datepicker td.active, .datepicker td.active:hover { + background-color: #357ebd; + border-color: #3071a9; + color: #fff; +} + +/* Introduced by Day8 from http://eternicode.github.io/bootstrap-datepicker */ +.datepicker td.today, .datepicker td.today:hover { + background-color: #ffcd70; + border-color: #f59e00; + border-radius: 18px; + color: #fff; +} + +.datepicker th.day-enabled, label.day-enabled { + font-weight: normal; + font-size: 10px; + color: #333; +} + +.datepicker th.selectable { + font-weight: normal; + color: #357ebd; +} + +.datepicker th.day-disabled { + font-weight: normal; + font-size: 10px; + color: #999; +} + +.datepicker td.week, .datepicker th.week { + font-size: 80%; + color: #ccc; +} + + +.datepicker th.prev { + border: 1px solid #ccc; + border-right-style: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: #F7F7F7; +} + +.datepicker th.next { + border: 1px solid #ccc; + border-left-style: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + background-color: #F7F7F7; +} + +.datepicker th.month { + width: auto; + font-size: 14px; + color: #777; + border-radius: 0; + border: 1px solid #CCC; + border-right-style: none; + border-left-style: none; + background-color: #F7F7F7; +} + +.dropdown-button { + cursor: pointer; + height: 32px; + font-size: 13px; + font-weight: normal; +} + +.dropdown-button.activator { + width: 40px; + color: #777; + background-color: #F7F7F7 +} + +/*---------------------------------------------------------------------------------------- + END OF DATE PICKER SECTION... + + + CSS THROBBER +----------------------------------------------------------------------------------------*/ +@keyframes blink { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(.5); + opacity: 0; + } +} +@-moz-keyframes blink { + 0% { + -moz-transform: scale(1); + opacity: 1; + } + 100% { + -moz-transform: scale(.5); + opacity: 0; + } +} +@-webkit-keyframes blink { + 0% { + -webkit-transform: scale(1); + opacity: 1; + } + 100% { + -webkit-transform: scale(.5); + opacity: 0; + } +} + +.loader { + position: relative; + display: inline-block; + vertical-align: middle; + width: 20px; + height: 20px; + margin: 20px; + padding: 0px; +} + +.loader li { + position: absolute; + display: block; + border-radius: 4px; + background-color: #999; + width: 6px; + height: 6px; + margin-top: -3px; + margin-left: -3px; + opacity: 0; + -webkit-animation: blink .8s ease infinite; + -moz-animation: blink .8s ease infinite; + animation: blink .8s ease infinite; +} + +.loader li:nth-child(1) { + top: 0%; + left: 50%; + -webkit-animation-delay: -.7s; + -moz-animation-delay: -.7s; + animation-delay: -.7s; +} + +.loader li:nth-child(2) { + top: 15%; + left: 85%; + -webkit-animation-delay: -.6s; + -moz-animation-delay: -.6s; + animation-delay: -.6s; +} + +.loader li:nth-child(3) { + top: 50%; + left: 100%; + -webkit-animation-delay: -.5s; + -moz-animation-delay: -.5s; + animation-delay: -.5s; +} + +.loader li:nth-child(4) { + top: 85%; + left: 85%; + -webkit-animation-delay: -.4s; + -moz-animation-delay: -.4s; + animation-delay: -.4s; +} + +.loader li:nth-child(5) { + top: 100%; + left: 50%; + -webkit-animation-delay: -.3s; + -moz-animation-delay: -.3s; + animation-delay: -.3s; +} + +.loader li:nth-child(6) { + top: 85%; + left: 15%; + -webkit-animation-delay: -.2s; + -moz-animation-delay: -.2s; + animation-delay: -.2s; +} + +.loader li:nth-child(7) { + top: 50%; + left: 0%; + -webkit-animation-delay: -.1s; + -moz-animation-delay: -.1s; + animation-delay: -.1s; + +} + +.loader li:nth-child(8) { + top: 15%; + left: 15%; +} + +.loader.smaller { + width: 9px; + height: 10px; + margin: 7px; +} + +.loader.smaller li { + width: 4px; + height: 4px; + margin-top: -2px; + margin-left: -2px; +} + +.loader.small { + width: 12px; + height: 12px; + margin: 10px; +} + +.loader.small li { + width: 4px; + height: 4px; + margin-top: -2px; + margin-left: -2px; +} + +.loader.large { + width: 48px; + height: 48px; + margin: 20px; +} + +.loader.large li { + border-radius: 14px; + width: 14px; + height: 14px; + margin-top: -7px; + margin-left: -7px; +} + +/*---------------------------------------------------------------------------------------- + END OF CSS THROBBER SECTION... + + + Bootstrap list-group-item variation (only used in selection-list component) +----------------------------------------------------------------------------------------*/ +/* TODO: re-demo CSS */ +.compact { + margin: 0; + height: 19px; + white-space: nowrap; + border: none; + padding: 0; + overflow: hidden; + cursor: default; + font-size: small; + text-overflow: ellipsis; +} +/* TODO: re-demo CSS */ +.list-group-item:hover { + background-color: #eeeeee; +} + +/*---------------------------------------------------------------------------------------- + Custom scrollbars + - http://css-tricks.com/custom-scrollbars-in-webkit + - http://codemug.com/html/custom-scrollbars-using-css +----------------------------------------------------------------------------------------*/ + +::-webkit-scrollbar { + width: 10px; + border-radius: 5px; + background-color: rgba(0,0,0,0.05); + } +::-webkit-scrollbar:horizontal { + height:10px; + } +::-webkit-scrollbar:hover { + background-color:rgba(0,0,0,0.20); + } +::-webkit-scrollbar-thumb:horizontal, ::-webkit-scrollbar-thumb:vertical { + background:rgba(0,0,0,0.25); + border-radius: 5px; + } +::-webkit-scrollbar-thumb:horizontal:active, ::-webkit-scrollbar-thumb:vertical:active { + background:rgba(0,0,0,0.45); + } + +/* Color of selected text*/ +/* +::-moz-selection { color: gold; background: red; } +::selection { color: gold; background: red; } +*/ + + +/*---------------------------------------------------------------------------------------- + Override bootstrap input text placeholder color (it's too dark) +----------------------------------------------------------------------------------------*/ + +.form-control::-webkit-input-placeholder { + color: #bbb; +} + +/*---------------------------------------------------------------------------------------- + Override bootstrap warning color (from horrible brown/orange to orange) +----------------------------------------------------------------------------------------*/ + +.has-warning .form-control { + border-color: #f57c00; +} + +.has-warning .form-control:focus { + border-color: #f57c00; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px rgba(245, 124, 0, 0.6); +} + +/* TODO: REMOVE */ +.has-warning .form-control-feedback { + color: #f57c00; +} + +.rc-input-text .zmdi-alert-triangle { + color: #f57c00; +} + +.has-error .form-control { + border-color: #d50000; +} + +.has-error .form-control:focus { + border-color: #d50000; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px rgba(213, 0, 0, 0.6); +} + +/* TODO: REMOVE */ +.has-error .form-control-feedback { + color: #d50000; +} + +/* Override bootstrap green validation */ + +.has-success .form-control { + border-color: #13C200; +} + +.has-success .form-control:focus { + border-color: #13C200; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px rgba(19, 194, 0, 0.6); +} + +.rc-input-text .zmdi-alert-circle { + color: #d50000; +} + +.rc-input-text .zmdi-check-circle { + color: #13C200; +} + +.alert-warning { + color: #ea6b00; + background-color: #ffecdc; + border-color: #faebcc; +} + +code { + margin-right: 0.2em; + border-radius: 4px; + color: #333; + background: #eee; + border: 1px solid #ddd; + padding: 0.1em 0.3em; +} + + +/*---------------------------------------------------------------------------------------- + Time component +----------------------------------------------------------------------------------------*/ + +.time-entry { + flex: none; + padding-left: 0.3em; + padding-top: 2px; + border: 1px solid #ccc; + border-radius: 4px 0 0 4px; + width: 3.3em; +} +.time-entry:last-child { + border-radius: 4px; +} +.time-icon { + display: flex; + padding: 0 0.3em; + border: 1px solid #ccc; + border-left: none; + border-radius: 0 4px 4px 0; + background-color: #ddd; +} + +/*---------------------------------------------------------------------------------------- + md-circle-icon-button +----------------------------------------------------------------------------------------*/ + +.rc-md-circle-icon-button { + width: 40px; /* material design dimensions */ + height: 40px; + text-align: center; /* to horizontally center the icon */ + border: 1px solid lightgrey; + border-radius: 50%; + background-color: inherit; + opacity: 0.9; +} + +.rc-md-circle-icon-button > i { + font-size : 24px; + line-height: 40px; /* to vertically center in surrounding :div */ + color: inherit; +} + +.rc-md-circle-icon-button:hover { + border: 1px solid #428bca; + color: #428bca; +} + +.rc-circle-smaller { + width: 26px; + height: 26px; +} + +.rc-circle-smaller > i { + font-size: 16px; + line-height: 24px; +} + +.rc-circle-larger { + width: 56px; + height: 56px; +} + +.rc-circle-larger > i { + font-size: 24px; + line-height: 56px; +} + +.rc-circle-emphasis { + border: 1px solid #428bca; + background-color: #428bca; + color: white; +} + +.rc-circle-emphasis:hover { + border: 1px solid #2196f3; + background-color: #2196f3; + color: white; +} + +.rc-circle-disabled { + border: none; +} + +.rc-circle-disabled:hover { + border: none; +} + +.rc-circle-disabled > i { + color: lightgrey; +} + +/*---------------------------------------------------------------------------------------- + md-icon-button +----------------------------------------------------------------------------------------*/ + +.rc-md-icon-button { + height: 24px; + font-size: 24px; + line-height: 24px; + text-align: center; /* to horizontally center the icon */ + background-color: inherit; + border-radius: 3px; +} + +.rc-md-icon-button > i { + color: inherit; +} + +.rc-md-icon-button:hover { + color: #2196f3; +} + +.rc-icon-smaller { + height: 16px; + font-size: 16px; + line-height: 16px; +} + +.rc-icon-larger { + height: 32px; + font-size: 32px; + line-height: 32px; +} + +.rc-icon-emphasis { + background-color: #428bca; + color: white; +} + +.rc-icon-emphasis:hover { + background-color: #2196f3; + color: white; +} + +.rc-icon-disabled > i { + color: lightgrey; +} + +/*---------------------------------------------------------------------------------------- + info-button + + CSS styles below are taken from https://github.com/oakmac/cljs.info... + + The MIT License (MIT) + Copyright (c) 2014 Chris Oakman and contributors + 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. +----------------------------------------------------------------------------------------*/ + +.rc-info-button { + padding-left: 3px; + padding-right: 3px; + fill: #bdbdbd; +} + +.rc-info-button:hover { + fill: #2962ff; +} + +.popover-content .info-heading { + font-size: 22px; +} + +.popover-content .info-subheading { + font-size: 18px; + margin-top: 8px; +} + +.popover-content .info-bold { + background-color: #555; + padding-left: 3px; + padding-right: 3px; + border-radius: 3px; + font-weight: bold; +} + +.popover-content a { + color: #ccf; +} + +.popover-content code { + color: white; + background-color: #555; + border: none; +} + +/*---------------------------------------------------------------------------------------- + row-button +----------------------------------------------------------------------------------------*/ + +.rc-row-button { + text-align: center; /* to horizontally center the icon */ + color: #2167f2; /2196f3; + background-color: inherit; + opacity: 0; + cursor: pointer; +} + +.rc-row-button > i { + color: inherit; +} + +.rc-row-mouse-over-row { + opacity: 0.4; +} + +.rc-row-button:hover, rc-row-mouse-over-row:hover { + opacity: 1; +} + +.rc-row-disabled { + opacity: 1; + color: lightgrey; + cursor: default; +} + +/*---------------------------------------------------------------------------------------- + div-table +----------------------------------------------------------------------------------------*/ + +.rc-div-table { + flex: none; + width: auto; + border: 2px solid lightgrey; + cursor: default; +} + +.rc-div-table-header > div { + background-color: #e8e8e8; + font-weight: bold; + padding: 5px; +} + +.rc-div-table-row { + border-top: 1px solid lightgrey; +} + +.rc-div-table-row:hover { + background-color: #f2f2f2; +} + +.rc-div-table-row > div { + padding: 5px; +} + +/*---------------------------------------------------------------------------------------- + General Typography +----------------------------------------------------------------------------------------*/ + +.semibold { + font-weight: 500; +} +.bold { + font-weight: 700; +} +.italic { + font-style: italic; +} +.uppercase { + text-transform: uppercase; +} +.all-small-caps { + font-variant: small-caps; +} + +.level1 { + font-family: Roboto, sans-serif; + font-weight: 200; + font-size: 40px; + color: rgba(68, 68, 68, 1.0); + letter-spacing: normal; + /*-ms-font-feature-settings: 'ss20' 1;*/ +} +.level2 { + font-family: Roboto, sans-serif; + font-weight: 200; + font-size: 26px; + color: rgba(0, 0, 0, 1.0); + letter-spacing: 0.001em; +} +.level3 { + font-family: Roboto, sans-serif; + font-weight: 500; + font-size: 15px; + color: rgba(68, 68, 68, 1.0); + letter-spacing: 0.002em; +} +.level4 { + font-family: Roboto, sans-serif; + font-weight: 500; + font-size: 15px; + color: rgba(68, 68, 68, 0.6); + letter-spacing: 0.002em; +} +.field-label { + font-family: Roboto, sans-serif; + font-weight: 400; + font-size: 14px; + color: rgba(68, 68, 68, 0.6); + letter-spacing: 0.002em; + font-variant: small-caps; + flex: none; +} + +.noselect { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +/*---------------------------------------------------------------------------------------- + Flexbox "display" styles + Requires 2 display values which we can't represent in a Clojure map +----------------------------------------------------------------------------------------*/ + +.display-flex { + display: -webkit-flex; + display: flex; +} +.display-inline-flex { + display: -webkit-inline-flex; + display: inline-flex; +} + +.zmdi-hc-fw-rc { + width: 1em; + text-align: center; +} + +.rc-typeahead-suggestions-container { + position: absolute; + width: 100%; + background-color: #eee; +} + +.rc-typeahead-suggestion { + background-color: #eee; + padding: 0.4em; +} + +.rc-typeahead-suggestion.active { + background-color: #ddd; +} diff --git a/resources/public/css/swingometer.css b/resources/public/css/swingometer.css new file mode 100644 index 0000000..fe3fe55 --- /dev/null +++ b/resources/public/css/swingometer.css @@ -0,0 +1,70 @@ +/***************************************************************************\ + * * + * swinging-needle-meter.css * + * * + * CSS styling for the swinging needle meter itself. * + * * +\***************************************************************************/ + +.snm-cursor { + stroke:#ff8500; + stroke-width: 3%; + stroke-opacity: 0.5; +} + +.snm-frame { + fill: none; + stroke-width: 5%; + stroke-linejoin: round; + stroke: #444444; +} + +.snm-gradation path { + stroke: black; + stroke-width: 1; +} + +.snm-gradation text { + font-size: 200%; + font-weight: lighter; +} + +.snm-hub { + fill: #444444; +} + +.snm-meter { + height: 50%; + width: auto; +} + +.snm-needle { + stroke: black; + stroke-width: 1; +} + +.snm-redzone { + fill:none; + stroke: maroon; + stroke-width: 10%; +} + +.snm-scale { + fill: none; + stroke: silver; + stroke-width: 10%; +} + +.snm-target .snm-frame { + stroke: green; +} + +.snm-value { + font-size: 400%; + font-weight: bold; + text-align: center; +} + +.snm-warning .snm-frame { + stroke: maroon; +} diff --git a/resources/public/index.html b/resources/public/index.html new file mode 100644 index 0000000..f4b2849 --- /dev/null +++ b/resources/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + Example swingometer following re-com conventions. + + +
+ + + + diff --git a/resources/swingometer.zip b/resources/swingometer.zip new file mode 100644 index 0000000..d667a48 Binary files /dev/null and b/resources/swingometer.zip differ diff --git a/src/clj/swingometer/core.clj b/src/clj/swingometer/core.clj new file mode 100644 index 0000000..074ad0f --- /dev/null +++ b/src/clj/swingometer/core.clj @@ -0,0 +1 @@ +(ns swingometer.core) diff --git a/src/cljs/swingometer/config.cljs b/src/cljs/swingometer/config.cljs new file mode 100644 index 0000000..28207bf --- /dev/null +++ b/src/cljs/swingometer/config.cljs @@ -0,0 +1,4 @@ +(ns swingometer.config) + +(def debug? + ^boolean goog.DEBUG) diff --git a/src/cljs/swingometer/core.cljs b/src/cljs/swingometer/core.cljs new file mode 100644 index 0000000..c3ff728 --- /dev/null +++ b/src/cljs/swingometer/core.cljs @@ -0,0 +1,23 @@ +(ns swingometer.core + (:require [reagent.core :as reagent] + [re-frame.core :as re-frame] + [swingometer.events] + [swingometer.subs] + [swingometer.views :as views] + [swingometer.config :as config])) + + +(defn dev-setup [] + (when config/debug? + (enable-console-print!) + (println "dev mode"))) + +(defn mount-root [] + (re-frame/clear-subscription-cache!) + (reagent/render [views/main-panel] + (.getElementById js/document "app"))) + +(defn ^:export init [] + (re-frame/dispatch-sync [:initialize-db]) + (dev-setup) + (mount-root)) diff --git a/src/cljs/swingometer/db.cljs b/src/cljs/swingometer/db.cljs new file mode 100644 index 0000000..01662fd --- /dev/null +++ b/src/cljs/swingometer/db.cljs @@ -0,0 +1,4 @@ +(ns swingometer.db) + +(def default-db + {:name "re-frame"}) diff --git a/src/cljs/swingometer/events.cljs b/src/cljs/swingometer/events.cljs new file mode 100644 index 0000000..36613fd --- /dev/null +++ b/src/cljs/swingometer/events.cljs @@ -0,0 +1,8 @@ +(ns swingometer.events + (:require [re-frame.core :as re-frame] + [swingometer.db :as db])) + +(re-frame/reg-event-db + :initialize-db + (fn [_ _] + db/default-db)) diff --git a/src/cljs/swingometer/subs.cljs b/src/cljs/swingometer/subs.cljs new file mode 100644 index 0000000..97d24be --- /dev/null +++ b/src/cljs/swingometer/subs.cljs @@ -0,0 +1,8 @@ +(ns swingometer.subs + (:require-macros [reagent.ratom :refer [reaction]]) + (:require [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :name + (fn [db] + (:name db))) diff --git a/src/cljs/swingometer/swingometer.cljs b/src/cljs/swingometer/swingometer.cljs new file mode 100644 index 0000000..04bfc5b --- /dev/null +++ b/src/cljs/swingometer/swingometer.cljs @@ -0,0 +1,231 @@ +(ns swingometer.swingometer + (:require [clojure.string :as string] + [re-com.core :refer [h-box v-box box gap line label title slider checkbox p]] + [re-com.box :refer [flex-child-style]] + [re-com.util :refer [deref-or-value]] + [re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]] + [reagent.core :as reagent])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; swingometer: an experiment in animating SVG from re-frame. +;;;; Draws heavily on re-com.. +;;;; +;;;; This program is free software; you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License +;;;; as published by the Free Software Foundation; either version 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2014 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; ------------------------------------------------------------------------------------ +;; Component: swingometer +;; ------------------------------------------------------------------------------------ + +;;; It seems the defaults given here are just documentation; the defaults +;;; that are actually used are those given in the :or clause of the argument map. +(def swingometer-args-desc + [{:name :model :required true :type "map | atom" + :validate-fn map? :description "A map mapping keys to maps of the following structure: {:id :snp :name \"Scottish National Party\" :colour \"yellow\" :votes 1234}"} + {:name :width :required false :type "integer" :default "300" + :validate-fn integer? :description "a CSS width"} + {:name :height :required false :type "integer" :default "200" + :validate-fn integer? :description "a CSS height"} + {:name :class :required false :type "string" + :validate-fn string? :description "CSS class names, space separated, for the top-level SVG element"} + {:name :frame-class :required false :type "string" :default "snm-frame" + :validate-fn string? :description "CSS class names, space separated, for the frame"} + {:name :hub-class :required false :type "string" :default "snm-hub" + :validate-fn string? :description "CSS class names, space separated, for the hub"} + {:name :scale-class :required false :type "string" :default "snm-scale" + :validate-fn string? :description "CSS class names, space separated, for the scale"} + {:name :id :required false :type "string" :default "meter" + :validate-fn string? :description "Element id for this instance of the meter"} + {:name :gradations :reduired false :type "integer" :default 5 + :validate-fn integer? :description "Number of gradations to show on the scale, not counting the point."} + {:name :style :required false :type "CSS style map" + :validate-fn css-style? :description "CSS styles to add or override"} + {:name :attr :required false :type "HTML attr map" + :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}]) + + +(def log (.-log js/console)) + + +(defn abs + "Return the absolute value of the (numeric) argument." + [n] (max n (- n))) + +;; the constant 140 represents the full sweep of the needle +;; from the left end of the scale to right end, in degrees. +(def full-scale-deflection 140) + + +(defn deflection + "Return the linear deflection of a needle given this `value` on the + range `min-value`...`total-votes`." + [value min-value max-value] + (let [range (- max-value min-value) + zero-offset (/ (- 0 min-value) range) + limited (min (max (+ zero-offset (/ value range)) 0) 1)] + (* (- limited 0.5) full-scale-deflection))) + + +(defn polar-to-cartesian + "Return, as a map with keys :x. :y, the cartesian coordinates at the point + `radius` distance at `theta` (degrees) angle from a point at + cartesian coordinates `cx`, `cy`." + [cx cy radius theta] + (let + [in-radians (/ (* (- theta 90) (aget js/Math "PI")) 180.0)] + {:x (+ cx (* radius (.cos js/Math in-radians))) + :y (+ cy (* radius (.sin js/Math in-radians)))})) + + +(defn describe-arc + "Return as a string an SVG path definition describing an arc centred + at `cx`, cy` starting at `start-angle` and ending at `end-angle` (both + angles in degrees)." + [cx cy radius start-angle end-angle] + (let + [start (polar-to-cartesian cx cy radius start-angle) + end (polar-to-cartesian cx cy radius end-angle) + large-arc? (if (<= (- end-angle start-angle) 180) 0 1) + sweep (if (> end-angle start-angle) 1 0)] + (string/join " " ["M" (:x start) (:y start) "A" radius radius 0 large-arc? sweep (:x end) (:y end)]))) + + +(defn as-label + "If this arg is a floating point number, format it to a reasonable width; else return it." + [arg] + (if + (and (number? arg) (not (integer? arg))) + (.toFixed arg 2) + arg)) + + +(defn gradation + "Return as a string an SVG path definition describing a radial stroke from a center + at `cx`, cy` starting at `min-radius` and extending to `max-radius`." + [cx cy min-radius max-radius angle label] + [:g {:class "snm-gradation" + :transform (string/join " " ["rotate(" angle cx cy ")"])} + [:path {:d (string/join + " " + ["M" + cx + (- cy + (+ min-radius + (* (- max-radius min-radius) 0.333))) + "L" + cx + (- cy max-radius)])}] + [:text {:text-anchor "start" + :x cx + :y (- cy min-radius)} (as-label label)]]) + + +(defn recursively-draw-segments + [still-to-do done total-votes cx cy radius] + (log (string/join " " ["\nstill-to-do" still-to-do "\ndone" done "\ntotal-votes" total-votes "cx" cx "cy" cy "radius" radius])) + (if + (empty? still-to-do) nil + (let [votes-done (reduce + (map :votes done)) + start-angle (deflection votes-done 0 total-votes) + party (first still-to-do) + end-angle (deflection (+ (:votes party) votes-done) 0 total-votes)] + (cons [:g [:path {:class "snm-scale" + :id (str (:id party) "-segment") + :style {:stroke (:colour party)} + :d (describe-arc cx cy radius start-angle end-angle)}] + (gradation cx cy (* radius 0.8) (* radius 1.1) start-angle + (let [vote-share (* (/ (:votes party) total-votes) 100)] + (str + (if (> vote-share 5) (name (:id party)) "") + (if (> vote-share 10) (str " " (int vote-share) "%")))))] + (recursively-draw-segments (rest still-to-do) (cons party done) total-votes cx cy radius))))) + + + +(defn swingometer + "Render an SVG swinging needle meter" + [& {:keys [model width height class scale-class frame-class id style attr] + :or {width 300 + height 200 + scale-class "snm-scale" + frame-class "snm-frame" + id "meter"} + :as args}] + {:pre [(validate-args-macro swingometer-args-desc args "swingometer")]} + (let [model (deref-or-value model) + mid-point-deflection (/ full-scale-deflection 2) + cx (/ width 2) + cy (* height 0.90) + needle-length (* height 0.75) + scale-radius (* height 0.7) + gradation-inner (* height 0.55) + gradations 5 + total-votes (reduce + (map #(:votes %) (vals model)))] + [box + :align :start + :child [:div + (merge + {:class (str "swingometer " class) + :style (merge (flex-child-style "none") + {:width width :height height} + style)} + attr) + [:svg {:xmlSpace "preserve" + :overflow "visible" + :viewBox (string/join " " [0 0 width height]) + :width (str width "px") + :height (str height "px") + :y "0px" + :x "0px" + :version "1.1" + :id id + :class (str "snm-meter " class)} + [:text + {:text-anchor "middle" + :x (/ width 2) + :y (/ height 2) + :width "100" + :id (str id "-total-votes") + :class "snm-value"}[:tspan (reduce + (map :votes (vals model)))]] + [:path {:class scale-class + :id (str id "-scale") + :d (describe-arc cx cy scale-radius + (- 0 mid-point-deflection) + mid-point-deflection)}] +;; (if (and (> gradations 0) (> total-votes 0)) +;; (apply vector (cons :g (map #(let +;; [value (* +;; (/ +;; total-votes +;; gradations) %)] +;; (gradation cx cy gradation-inner needle-length +;; (deflection value 0 total-votes) +;; value)) +;; (range 0 (+ gradations 1)))))) + (apply vector + (cons :g (recursively-draw-segments (map model (sort (keys model))) nil total-votes cx cy scale-radius))) + [:rect {:class frame-class + :id (str id "-frame") + :x (* width 0.05) :y (* height .05) :height cy :width (* width 0.9)}] +;; [:circle {:class hub-class +;; :id (str id "-hub") +;; :r (/ height 10) :cx cx :cy cy}] + ] + ]])) diff --git a/src/cljs/swingometer/utils.cljs b/src/cljs/swingometer/utils.cljs new file mode 100644 index 0000000..e6b8dde --- /dev/null +++ b/src/cljs/swingometer/utils.cljs @@ -0,0 +1,115 @@ +(ns swingometer.utils + (:require [re-com.core :refer [h-box v-box box gap title line label hyperlink-href align-style]])) + +;;;; This file is just stolen wholesale from re-demo in the re-com package; +;;;; I claim no credit for it. + +(defn github-hyperlink + "given a label and a relative path, return a component which hyperlinks to the GitHub URL in a new tab" + [label src-path] + (let [base-url (str "https://github.com/Day8/re-com/tree/master/")] + [hyperlink-href + :label label + ;:style {:font-size "13px"} + :href (str base-url src-path) + :target "_blank"])) + +(defn panel-title + "Shown across the top of each page" + [panel-name src1 src2] + [v-box + :children [[h-box + :margin "0px 0px 9px 0px" + :height "54px" + :align :end + :children [[title + :label panel-name + :level :level1 + :margin-bottom "0px" + :margin-top "2px"] + [gap :size "25px"] + (when src1 [h-box + :class "all-small-caps" + :gap "7px" + :align :center + :children [ + [label :label "source:" ] + [github-hyperlink "component" src1] + [label :label "|" :style {:font-size "12px"}] + ;[line] + [github-hyperlink "page" src2]]])]] + [line]]]) + +(defn title2 + "2nd level title" + [text style] + [title + :label text + :level :level2 + :style style]) + +(defn status-text + "given some status text, return a component that displays that status" + [status style] + [:span + [:span.bold "Status: "] + [:span {:style style} status]]) + +(defn material-design-hyperlink + [text] + [hyperlink-href + :label text + :href "http://zavoloklom.github.io/material-design-iconic-font/icons.html" + :target "_blank"]) + + + +(defn arg-row + "I show one argument in an args table." + [name-width arg odd-row?] + (let [required (:required arg) + default (:default arg) + arg-type (:type arg) + needed-vec (if (not required) + (if (nil? default) + [[:span.semibold.all-small-caps "optional"]] + [[:span.semibold.all-small-caps "default:"] [:span.semibold (str default)]]) + [[:span.semibold.all-small-caps "required"]])] + [h-box + :style {:background (if odd-row? "#F4F4F4" "#FCFCFC")} + :children [[:span {:class "semibold" + :style (merge (align-style :align-self :center) + {:width name-width + :padding-left "15px"})} + (str (:name arg))] + [line :size "1px" :color "white"] + [v-box + :style {:padding "7px 15px 2px 15px"} + :gap "4px" + :width "310px" + :children [[h-box + :gap "4px" + :children (concat [[:span.semibold arg-type] + [gap :size "10px"]] + needed-vec)] + [:p + {:font-size "smaller" :color "red"} + (:description arg)]]]]])) + + +(defn args-table + "I display a component arguements in an easy to read format" + [args] + (let [name-width "130px"] + (fn + [] + [v-box + :children (concat + [[title2 "Parameters"] + [gap :size "10px"]] + (map (partial arg-row name-width) args (cycle [true false])))]))) + + +(defn scroll-to-top + [element] + (set! (.-scrollTop element) 0)) diff --git a/src/cljs/swingometer/views.cljs b/src/cljs/swingometer/views.cljs new file mode 100644 index 0000000..44914f4 --- /dev/null +++ b/src/cljs/swingometer/views.cljs @@ -0,0 +1,137 @@ +(ns swingometer.views + (:require [re-frame.core :as re-frame] + [re-com.core :refer [h-box v-box box gap line label title progress-bar slider checkbox p single-dropdown]] + [re-com.util :refer [deref-or-value]] + [swingometer.swingometer :refer [swingometer swingometer-args-desc]] + [swingometer.utils :refer [panel-title title2 args-table github-hyperlink status-text]] + [reagent.core :as reagent])) + +(defn swingometer-demo + [] + (let [model (reagent/atom {:snp {:id :snp :name "Scottish National Party" :colour "yellow" :votes 10} + :lab {:id :lab :name "Labour Party" :colour "red" :votes 10} + :con {:id :con :name "Conservative Party" :colour "blue" :votes 10} + :ld {:id :ld :name "Liberal Democrats" :colour "GoldenRod" :votes 10} + :grn {:id :grn :name "Scottish Green Party" :colour "green" :votes 10} + :ukp {:id :ukp :name "United Kingdom Independence Party" :colour "DarkViolet" :votes 10}})] + (fn + [] + [v-box + :size "auto" + :gap "10px" + :children [[panel-title "Swingometer"] + [h-box + :gap "100px" + :children [[v-box + :gap "10px" + :width "450px" + :children [[title2 "Notes"] + [status-text "Wildly experimental"] + [p "An SVG swingometer intended to be useful in elections."] + + [title2 "Behaviour"] + + + [args-table swingometer-args-desc]]] + [v-box + :gap "10px" + :children [[title2 "Demo"] + [v-box + :gap "20px" + :children [[swingometer + :model model + :height 600 + :width 1000] + [title :level :level3 :label "Parameters"] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:con (deref-or-value model)))]] + [slider + :model (:votes (:con (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:con (merge (:con (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:con (deref-or-value model)))]]] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:grn (deref-or-value model)))]] + [slider + :model (:votes (:grn (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:grn (merge (:grn (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:grn (deref-or-value model)))]]] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:lab (deref-or-value model)))]] + [slider + :model (:votes (:lab (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:lab (merge (:lab (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:lab (deref-or-value model)))]]] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:ld (deref-or-value model)))]] + [slider + :model (:votes (:ld (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:ld (merge (:ld (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:ld (deref-or-value model)))]]] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:snp (deref-or-value model)))]] + [slider + :model (:votes (:snp (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:snp (merge (:snp (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:snp (deref-or-value model)))]]] + [h-box + :gap "10px" + :children [[box :align :start :child [:label (:name (:ukp (deref-or-value model)))]] + [slider + :model (:votes (:ukp (deref-or-value model))) + :min 0 + :max 1000 + :width "200px" + :on-change #(reset! model + (merge (deref-or-value model) + {:ukp (merge (:ukp (deref-or-value model)) + {:votes %})}))] + [label :label (:votes (:ukp (deref-or-value model)))]]] + ]]]]]]]]))) + + +;; core holds a reference to panel, so need one level of indirection to get figwheel updates +(defn panel + [] + [swingometer-demo]) + + +(defn main-panel [] + (fn [] + [v-box + :height "100%" + :children [[swingometer-demo]]]))