├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── project.clj ├── src └── datascript_todo │ ├── core.clj │ ├── core.cljs │ ├── dom.cljs │ └── util.cljs ├── target └── todo.js └── todo.css /.gitignore: -------------------------------------------------------------------------------- 1 | /target/** 2 | !/target/todo.js 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /dev/out/* 12 | /dev/*.js 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2015 Nikita Prokopov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataScript ToDo Sample Application 2 | 3 | ### Development Build 4 | 5 | ``` 6 | lein cljsbuild auto none & 7 | open index.html 8 | ``` 9 | 10 | ### For LightTable UI 11 | 12 | For inline evaluation of clojurescript within LightTable you must manually 13 | update LightTable's Clojure plugin to 0.2.0 or later. As of this writing, 14 | the plugin version is 0.1.0 and does not update unless, from the LightTable 15 | UI, you delete the plugin and re-install it. 16 | 17 | Build from the command line before using LightTable. 18 | 19 | It's important to use `none` to invoke the correct cljsbuild identifier, 20 | otherwise advanced optimizations will prevent LightTable from finding `goog` 21 | 22 | ### License 23 | 24 | MIT License 25 | 26 | Copyright © 2015 Nikita Prokopov 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ToDo 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject datascript-todo "0.1.0" 2 | :dependencies [ 3 | [org.clojure/clojure "1.7.0"] 4 | [org.clojure/clojurescript "1.7.122"] 5 | [datascript "0.13.0"] 6 | [datascript-transit "0.2.0"] 7 | [rum "0.4.0"] 8 | ] 9 | 10 | :plugins [ 11 | [lein-cljsbuild "1.1.0"] 12 | ] 13 | 14 | :cljsbuild { 15 | :builds [ 16 | { :id "advanced" 17 | :source-paths ["src"] 18 | :compiler { 19 | :main datascript-todo.core 20 | :output-to "target/todo.js" 21 | :optimizations :advanced 22 | :pretty-print false 23 | }} 24 | ]} 25 | 26 | :profiles { 27 | :dev { 28 | :cljsbuild { 29 | :builds [ 30 | { :id "none" 31 | :source-paths ["src"] 32 | :compiler { 33 | :main datascript-todo.core 34 | :output-to "target/todo.js" 35 | :output-dir "target/none" 36 | :optimizations :none 37 | :source-map true 38 | }} 39 | ]} 40 | } 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /src/datascript_todo/core.clj: -------------------------------------------------------------------------------- 1 | (ns datascript-todo.core) 2 | 3 | (defmacro profile [k & body] 4 | `(let [k# ~k] 5 | (.time js/console k#) 6 | (let [res# (do ~@body)] 7 | (.timeEnd js/console k#) 8 | res#))) 9 | -------------------------------------------------------------------------------- /src/datascript_todo/core.cljs: -------------------------------------------------------------------------------- 1 | (ns datascript-todo.core 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.string :as str] 5 | [datascript.core :as d] 6 | [rum.core :as rum] 7 | [datascript.transit :as dt] 8 | [datascript-todo.dom :as dom] 9 | [datascript-todo.util :as u]) 10 | (:require-macros 11 | [datascript-todo.core :refer [profile]])) 12 | 13 | (enable-console-print!) 14 | 15 | (def schema {:todo/tags {:db/cardinality :db.cardinality/many} 16 | :todo/project {:db/valueType :db.type/ref} 17 | :todo/done {:db/index true} 18 | :todo/due {:db/index true}}) 19 | (defonce conn (d/create-conn schema)) 20 | 21 | (declare render persist) 22 | 23 | (defn reset-conn! [db] 24 | (reset! conn db) 25 | (render db) 26 | (persist db)) 27 | 28 | ;; Entity with id=0 is used for storing auxilary view information 29 | ;; like filter value and selected group 30 | 31 | (defn set-system-attrs! [& args] 32 | (d/transact! conn 33 | (for [[attr value] (partition 2 args)] 34 | (if value 35 | [:db/add 0 attr value] 36 | [:db.fn/retractAttribute 0 attr])))) 37 | 38 | (defn system-attr 39 | ([db attr] 40 | (get (d/entity db 0) attr)) 41 | ([db attr & attrs] 42 | (mapv #(system-attr db %) (concat [attr] attrs)))) 43 | 44 | ;; History 45 | 46 | (defonce history (atom [])) 47 | (def ^:const history-limit 10) 48 | 49 | ;; Keyword filter 50 | 51 | (rum/defc filter-pane [db] 52 | [:.filter-pane 53 | [:input.filter {:type "text" 54 | :value (or (system-attr db :system/filter) "") 55 | :on-change (fn [_] 56 | (set-system-attrs! :system/filter (dom/value (dom/q ".filter")))) 57 | :placeholder "Filter"}]]) 58 | 59 | ;; Rules are used to implement OR semantic of a filter 60 | ;; ?term must match either :project/name OR :todo/tags 61 | (def filter-rule 62 | '[[(match ?todo ?term) 63 | [?todo :todo/project ?p] 64 | [?p :project/name ?term]] 65 | [(match ?todo ?term) 66 | [?todo :todo/tags ?term]]]) 67 | 68 | ;; terms are passed as a collection to query, 69 | ;; each term futher interpreted with OR semantic 70 | (defn todos-by-filter [db terms] 71 | (d/q '[:find [?e ...] 72 | :in $ % [?term ...] 73 | :where [?e :todo/text] 74 | (match ?e ?term)] 75 | db filter-rule terms)) 76 | 77 | (defn filter-terms [db] 78 | (not-empty 79 | (str/split (system-attr db :system/filter) #"\s+"))) 80 | 81 | (defn filtered-db [db] 82 | (if-let [terms (filter-terms db)] 83 | (let[whitelist (set (todos-by-filter db terms)) 84 | pred (fn [db datom] 85 | (or (not= "todo" (namespace (:a datom))) 86 | (contains? whitelist (:e datom))))] 87 | (d/filter db pred)) 88 | db)) 89 | 90 | ;; Groups 91 | 92 | (defmulti todos-by-group (fn [db group item] group)) 93 | 94 | ;; Datalog has no negative semantic (NOT IN), we emulate it 95 | ;; with get-else (get attribute with default value), and then 96 | ;; filtering by that attribute, keeping only todos that resulted 97 | ;; into default value 98 | (defmethod todos-by-group :inbox [db _ _] 99 | (d/q '[:find [?todo ...] 100 | :where [?todo :todo/text] 101 | [(get-else $ ?todo :todo/project :none) ?project] 102 | [(get-else $ ?todo :todo/due :none) ?due] 103 | [(= ?project :none)] 104 | [(= ?due :none)]] 105 | db)) 106 | 107 | (defmethod todos-by-group :completed [db _ _] 108 | (d/q '[:find [?todo ...] 109 | :where [?todo :todo/done true]] 110 | db)) 111 | 112 | (defmethod todos-by-group :all [db _ _] 113 | (d/q '[:find [?todo ...] 114 | :where [?todo :todo/text]] 115 | db)) 116 | 117 | (defmethod todos-by-group :project [db _ pid] 118 | (d/q '[:find [?todo ...] 119 | :in $ ?pid 120 | :where [?todo :todo/project ?pid]] 121 | db pid)) 122 | 123 | ;; Since todos do not store month directly, we pass in 124 | ;; month boundaries and then filter todos with <= predicate 125 | (defmethod todos-by-group :month [db _ [year month]] 126 | (d/q '[:find [?todo ...] 127 | :in $ ?from ?to 128 | :where [?todo :todo/due ?due] 129 | [(<= ?from ?due ?to)]] 130 | db (u/month-start month year) (u/month-end month year))) 131 | 132 | (rum/defc group-item [db title group item] 133 | ;; Joining DB with a collection 134 | (let [todos (todos-by-group db group item) 135 | count (d/q '[:find (count ?todo) . 136 | :in $ [?todo ...] 137 | :where [$ ?todo :todo/done false]] 138 | db todos)] 139 | [:.group-item {:class (when (= [group item] 140 | (system-attr db :system/group :system/group-item)) 141 | "group-item_selected")} 142 | [:span {:on-click (fn [_] 143 | (set-system-attrs! :system/group group 144 | :system/group-item item)) } 145 | title] 146 | (when count 147 | [:span.group-item-count count])])) 148 | 149 | (rum/defc plan-group [db] 150 | [:.group 151 | [:.group-title "Plan"] 152 | ;; Here we’re calculating month inside a query via passed in function 153 | (for [[year month] (->> (d/q '[:find [?month ...] 154 | :in $ ?date->month 155 | :where [?todo :todo/due ?date] 156 | [(?date->month ?date) ?month]] 157 | db u/date->month) 158 | sort)] 159 | (group-item db (u/format-month month year) :month [year month]))]) 160 | 161 | (rum/defc projects-group [db] 162 | [:.group 163 | [:.group-title "Projects"] 164 | (for [[pid name] (->> (d/q '[:find ?pid ?project 165 | :where [?todo :todo/project ?pid] 166 | [?pid :project/name ?project]] 167 | db) 168 | (sort-by second))] 169 | (group-item db name :project pid))]) 170 | 171 | (rum/defc overview-pane [db] 172 | [:.overview-pane 173 | [:.group 174 | (group-item db "Inbox" :inbox nil) 175 | (group-item db "Completed" :completed nil) 176 | (group-item db "All" :all nil)] 177 | (plan-group db) 178 | (projects-group db)]) 179 | 180 | ;; This transaction function swaps the value of :todo/done attribute. 181 | ;; Transaction funs are handy in situations when to decide what to do 182 | ;; you need to analyse db first. They deliver atomicity and linearizeability 183 | ;; to such calculations 184 | (defn toggle-todo-tx [db eid] 185 | (let [done? (:todo/done (d/entity db eid))] 186 | [[:db/add eid :todo/done (not done?)]])) 187 | 188 | (defn toggle-todo [eid] 189 | (d/transact! conn [[:db.fn/call toggle-todo-tx eid]])) 190 | 191 | (rum/defc todo-pane [db] 192 | [:.todo-pane 193 | (let [todos (let [[group item] (system-attr db :system/group :system/group-item)] 194 | (todos-by-group db group item))] 195 | (for [eid (sort todos) 196 | :let [td (d/entity db eid)]] 197 | [:.todo {:class (if (:todo/done td) "todo_done" "")} 198 | [:.todo-checkbox {:on-click #(toggle-todo eid)} "✔︎"] 199 | [:.todo-text (:todo/text td)] 200 | [:.todo-subtext 201 | (when-let [due (:todo/due td)] 202 | [:span (.toDateString due)]) 203 | ;; here we’re using entity ref navigation, going from 204 | ;; todo (td) to project to project/name 205 | (when-let [project (:todo/project td)] 206 | [:span (:project/name project)]) 207 | (for [tag (:todo/tags td)] 208 | [:span tag])]]))]) 209 | 210 | (defn extract-todo [] 211 | (when-let [text (dom/value (dom/q ".add-text"))] 212 | {:text text 213 | :project (dom/value (dom/q ".add-project")) 214 | :due (dom/date-value (dom/q ".add-due")) 215 | :tags (dom/array-value (dom/q ".add-tags"))})) 216 | 217 | (defn clean-todo [] 218 | (dom/set-value! (dom/q ".add-text") nil) 219 | (dom/set-value! (dom/q ".add-project") nil) 220 | (dom/set-value! (dom/q ".add-due") nil) 221 | (dom/set-value! (dom/q ".add-tags") nil)) 222 | 223 | (defn add-todo [] 224 | (when-let [todo (extract-todo)] 225 | ;; This is slightly complicated logic where we need to identify 226 | ;; if a project with such name already exist. If yes, we need its 227 | ;; id to reference from entity, if not, we need to create it first 228 | ;; and then use its id to reference. We’re doing both in a single 229 | ;; transaction to avoid inconsistencies 230 | (let [project (:project todo) 231 | project-id (when project (u/e-by-av @conn :project/name project)) 232 | project-tx (when (and project (nil? project-id)) 233 | [[:db/add -1 :project/name project]]) 234 | entity (->> {:todo/text (:text todo) 235 | :todo/done false 236 | :todo/project (when project (or project-id -1)) 237 | :todo/due (:due todo) 238 | :todo/tags (:tags todo)} 239 | (u/remove-vals nil?))] 240 | (d/transact! conn (concat project-tx [entity]))) 241 | (clean-todo))) 242 | 243 | (rum/defc add-view [] 244 | [:form.add-view {:on-submit (fn [_] (add-todo) false)} 245 | [:input.add-text {:type "text" :placeholder "New task"}] 246 | [:input.add-project {:type "text" :placeholder "Project"}] 247 | [:input.add-tags {:type "text" :placeholder "Tags"}] 248 | [:input.add-due {:type "text" :placeholder "Due date"}] 249 | [:input.add-submit {:type "submit" :value "Add task"}]]) 250 | 251 | (rum/defc history-view [db] 252 | [:.history-view 253 | (for [state @history] 254 | [:.history-state 255 | { :class (when (identical? state db) "history-selected") 256 | :on-click (fn [_] (reset-conn! state)) }]) 257 | (if-let [prev (u/find-prev @history #(identical? db %))] 258 | [:button.history-btn {:on-click (fn [_] (reset-conn! prev))} "‹ undo"] 259 | [:button.history-btn {:disabled true} "‹ undo"]) 260 | (if-let [next (u/find-next @history #(identical? db %))] 261 | [:button.history-btn {:on-click (fn [_] (reset-conn! next))} "redo ›"] 262 | [:button.history-btn {:disabled true} "redo ›"])]) 263 | 264 | (rum/defc canvas [db] 265 | [:.canvas 266 | [:.main-view 267 | (filter-pane db) 268 | (let [db (filtered-db db)] 269 | (list 270 | (overview-pane db) 271 | (todo-pane db)))] 272 | (add-view) 273 | (history-view db)]) 274 | 275 | (defn render 276 | ([] (render @conn)) 277 | ([db] 278 | (profile "render" 279 | (rum/mount (canvas db) js/document.body)))) 280 | 281 | ;; re-render on every DB change 282 | (d/listen! conn :render 283 | (fn [tx-report] 284 | (render (:db-after tx-report)))) 285 | 286 | ;; logging of all transactions (prettified) 287 | (d/listen! conn :log 288 | (fn [tx-report] 289 | (let [tx-id (get-in tx-report [:tempids :db/current-tx]) 290 | datoms (:tx-data tx-report) 291 | datom->str (fn [d] (str (if (:added d) "+" "−") 292 | "[" (:e d) " " (:a d) " " (pr-str (:v d)) "]"))] 293 | (println 294 | (str/join "\n" (concat [(str "tx " tx-id ":")] (map datom->str datoms))))))) 295 | 296 | ;; history 297 | 298 | (d/listen! conn :history 299 | (fn [tx-report] 300 | (let [{:keys [db-before db-after]} tx-report] 301 | (when (and db-before db-after) 302 | (swap! history (fn [h] 303 | (-> h 304 | (u/drop-tail #(identical? % db-before)) 305 | (conj db-after) 306 | (u/trim-head history-limit)))))))) 307 | 308 | ;; transit serialization 309 | 310 | (defn db->string [db] 311 | (profile "db serialization" 312 | (dt/write-transit-str db))) 313 | 314 | (defn string->db [s] 315 | (profile "db deserialization" 316 | (dt/read-transit-str s))) 317 | 318 | ;; persisting DB between page reloads 319 | (defn persist [db] 320 | (js/localStorage.setItem "datascript-todo/DB" (db->string db))) 321 | 322 | (d/listen! conn :persistence 323 | (fn [tx-report] ;; FIXME do not notify with nil as db-report 324 | ;; FIXME do not notify if tx-data is empty 325 | (when-let [db (:db-after tx-report)] 326 | (js/setTimeout #(persist db) 0)))) 327 | 328 | ;; restoring once persisted DB on page load 329 | (or 330 | (when-let [stored (js/localStorage.getItem "datascript-todo/DB")] 331 | (let [stored-db (string->db stored)] 332 | (when (= (:schema stored-db) schema) ;; check for code update 333 | (reset-conn! stored-db) 334 | (swap! history conj @conn) 335 | true))) 336 | (d/transact! conn u/fixtures)) 337 | 338 | #_(js/localStorage.clear) 339 | 340 | ;; for interactive re-evaluation 341 | (render) 342 | 343 | 344 | -------------------------------------------------------------------------------- /src/datascript_todo/dom.cljs: -------------------------------------------------------------------------------- 1 | (ns datascript-todo.dom 2 | (:require 3 | [clojure.string :as str])) 4 | 5 | (defn q [selector] 6 | (js/document.querySelector selector)) 7 | 8 | (defn set-value! [el value] 9 | (set! (.-value el) value)) 10 | 11 | (defn value [el] 12 | (let [val (.-value el)] 13 | (when-not (str/blank? val) 14 | (str/trim val)))) 15 | 16 | (defn date-value [el] 17 | (when-let [val (value el)] 18 | (let [val (js/Date.parse val)] 19 | (when-not (js/isNaN val) 20 | (js/Date. val))))) 21 | 22 | (defn array-value [el] 23 | (when-let [val (value el)] 24 | (str/split val #"\s+"))) 25 | -------------------------------------------------------------------------------- /src/datascript_todo/util.cljs: -------------------------------------------------------------------------------- 1 | (ns datascript-todo.util 2 | (:require 3 | [datascript.core :as d])) 4 | 5 | (defn remove-vals [f m] 6 | (reduce-kv (fn [m k v] (if (f v) m (assoc m k v))) (empty m) m)) 7 | 8 | (defn find-prev [xs pred] 9 | (last (take-while #(not (pred %)) xs))) 10 | 11 | (defn find-next [xs pred] 12 | (fnext (drop-while #(not (pred %)) xs))) 13 | 14 | (defn drop-tail [xs pred] 15 | (loop [acc [] 16 | xs xs] 17 | (let [x (first xs)] 18 | (cond 19 | (nil? x) acc 20 | (pred x) (conj acc x) 21 | :else (recur (conj acc x) (next xs)))))) 22 | 23 | (defn trim-head [xs n] 24 | (vec (drop (- (count xs) n) xs))) 25 | 26 | (defn index [xs] 27 | (map vector xs (range))) 28 | 29 | (defn e-by-av [db a v] 30 | (-> (d/datoms db :avet a v) first :e)) 31 | 32 | (defn date->month [date] 33 | [(.getFullYear date) 34 | (inc (.getMonth date))]) 35 | 36 | (defn format-month [month year] 37 | (str (get ["January" 38 | "February" 39 | "March" 40 | "April" 41 | "May" 42 | "June" 43 | "July" 44 | "August" 45 | "September" 46 | "October" 47 | "November" 48 | "December"] (dec month)) 49 | " " year)) 50 | 51 | (defn month-start [month year] 52 | (js/Date. year (dec month) 1)) 53 | 54 | (defn month-end [month year] 55 | (let [[month year] (if (< month 12) 56 | [(inc month) year] 57 | [1 (inc year)])] 58 | (-> (js/Date. year (dec month) 1) 59 | .getTime 60 | dec 61 | js/Date. 62 | ))) 63 | 64 | (def fixtures [ 65 | [:db/add 0 :system/group :all] 66 | {:db/id -1 67 | :project/name "datascript"} 68 | {:db/id -2 69 | :project/name "nyc-webinar"} 70 | {:db/id -3 71 | :project/name "shopping"} 72 | 73 | {:todo/text "Displaying list of todos" 74 | :todo/tags ["listen" "query"] 75 | :todo/project -2 76 | :todo/done true 77 | :todo/due #inst "2014-12-13"} 78 | {:todo/text "Persisting to localStorage" 79 | :todo/tags ["listen" "serialization" "transact"] 80 | :todo/project -2 81 | :todo/done true 82 | :todo/due #inst "2014-12-13"} 83 | {:todo/text "Make task completable" 84 | :todo/tags ["transact" "funs"] 85 | :todo/project -2 86 | :todo/done false 87 | :todo/due #inst "2014-12-13"} 88 | {:todo/text "Fix fn calls on emtpy rels" 89 | :todo/tags ["bug" "funs" "query"] 90 | :todo/project -1 91 | :todo/done false 92 | :todo/due #inst "2015-01-01"} 93 | {:todo/text "Add db filtering" 94 | :todo/project -1 95 | :todo/done false 96 | :todo/due #inst "2015-05-30"} 97 | {:todo/text "Soap" 98 | :todo/project -3 99 | :todo/done false 100 | :todo/due #inst "2015-05-01"} 101 | {:todo/text "Cake" 102 | :todo/done false 103 | :todo/project -3} 104 | {:todo/text "Just a task" :todo/done false} 105 | {:todo/text "Another incomplete task" :todo/done false}]) 106 | -------------------------------------------------------------------------------- /todo.css: -------------------------------------------------------------------------------- 1 | * { box-sizing: border-box; vertical-align: top; text-rendering: optimizelegibility; } 2 | tr, td, table, tbody { padding: 0; margin: 0; border-collapse: collapse; } 3 | 4 | html, input, textarea, button { font: 13px/20px 'Input Sans Narr', sans-serif; } 5 | html, body { width: 100%; height: 100%; margin: 0; padding: 0; } 6 | body { background: linear-gradient(to bottom, hsl(185, 45%, 63%) 0%, hsl(185, 45%, 58%) 100%); background-attachment: fixed; } 7 | 8 | input[type=text] { border: none; border-bottom: 2px solid #cfcfcf; background: transparent; } 9 | input[type=text]:focus { border-color: hsl(4, 74%, 65%); } 10 | input[type=text], button { outline: none; } 11 | 12 | .todo-checkbox, .group-item, .history-view { -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; } 13 | 14 | .canvas { width: 1200px; margin: 40px auto; } 15 | .main-view { width: 700px; } 16 | .add-view { width: 400px; margin-left: 40px; } 17 | .main-view, .add-view { background: linear-gradient(to bottom, #fffffc 0%, #fafaf4 100%); border-radius: 9px; display: inline-block; box-shadow: 0 3px 10px hsla(0,0%,50%,0.9); border: 4px solid #f9bf3b; border-color: hsl(4, 74%, 65%); } 18 | 19 | .filter { font-size: 20px; line-height: 30px; width: 650px; margin: 20px auto; display: block; } 20 | 21 | .overview-pane, .todo-pane { display: inline-block; } 22 | 23 | .overview-pane { width: 200px; padding: 0 0 10px; } 24 | .group { margin: 0 0 20px 0; } 25 | .group-title, .group-item { padding: 4px 20px 0px; line-height: 20px; margin: -2px 0; } 26 | .group-title { text-transform: uppercase; color: #9f9f9f; margin-bottom: 0px; } 27 | .group-item-count { float: right; display: inline-block; padding: 2px 6px 0px; border-radius: 16px; font-size: 10px; line-height: 16px; border: none; position: relative; top: -1px; } 28 | .group-item > span:first-child { cursor: pointer; } 29 | 30 | .group-item_empty { color: #bbb; } 31 | .group-item-count, .group-item_selected > span:first-child, button, input[type=submit] { background: linear-gradient(to bottom, hsl(4, 74%, 70%) 0%, hsl(4, 74%, 65%) 100%); color: white; } 32 | .group-item_selected > span:first-child { padding: 6px 7px 4px 6px; margin: -6px -7px -4px -6px; border-radius: 3px; } 33 | 34 | .todo-pane { padding: 4px 0 0 20px; } 35 | .todo { margin: 0 0 20px 0; } 36 | .todo-text { font-size: 20px; margin-left: 26px; } 37 | .todo-subtext { font-size: 10px; margin-left: 26px; } 38 | .todo-subtext > span + span:before { content: "·"; margin: 0 3px; } 39 | 40 | .todo-checkbox { width: 18px; height: 18px; border: 2px solid #f9bf3b; border-radius: 4px; float: left; font-size: 20px; line-height: 14px; padding: 0 0 0 2px; cursor: pointer; border-color: hsl(4, 74%, 65%);} 41 | .todo-checkbox { color: transparent; } 42 | .todo-checkbox:hover { background: hsl(4, 74%, 80%); } 43 | .todo_done > .todo-checkbox { color: #ccc; background: hsl(4, 74%, 94%); border-color: hsl(4, 74%, 94%); } 44 | .todo_done > .todo-checkbox:hover { background: hsl(4, 74%, 97%); } 45 | .todo_done { color: #bbb; } 46 | 47 | .add-view { padding: 0 20px 20px; } 48 | .add-view > input[type=text] { width: 340px; margin: 20px 0; display: block; } 49 | .add-view > input[type=text]:focus { outline: none; border-color: hsl(4, 74%, 65%); } 50 | .add-text { font-size: 20px; line-height: 30px; } 51 | input[type=text].add-due { width: 101px; } 52 | 53 | .add-submit { border: none; border-radius: 4px; padding: 6px 12px 2px; cursor: pointer; } 54 | .add-submit:focus { outline: none; } 55 | .add-submit:active { box-shadow: 0 2px 4px hsla(0,0%,0%,0.2) inset; position: relative; top: 1px; } 56 | 57 | .history-view { margin: 10px 0; line-height: 22px; } 58 | .history-btn { border:none; background: transparent; cursor: pointer; } 59 | .history-btn[disabled] { opacity: 0.2; cursor: default; } 60 | .history-state { margin: 0 4px; width: 14px; height: 14px; border:none; background: white; opacity:0.3; display: inline-block; vertical-align: middle; cursor: pointer; } 61 | .history-selected { opacity: 1; } 62 | --------------------------------------------------------------------------------