├── .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 |
--------------------------------------------------------------------------------