├── .gitattributes
├── .gitignore
├── README.md
├── boot.properties
├── build.boot
├── docs
├── css
│ └── app.css
├── index.html
└── js
│ ├── app.cljs.edn
│ └── app.js
├── resources
├── css
│ └── app.css
├── index.html
└── js
│ └── app.cljs.edn
└── src
└── speccards
└── app.cljs
/.gitattributes:
--------------------------------------------------------------------------------
1 | resources/* linguist-vendored
2 | docs/* linguist-vendored
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.nrepl-port
3 | /out
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clojure.spec + devcards
2 |
3 | ## Rationale
4 |
5 | If you annotate with clojure.spec your UI and your app state you can use test.check generators to create random data that conforms to that spec.
6 |
7 | With devcards you can mount multiple versions of your React component to do visual testing. Therefore why not mounting several versions that display your randomly valid data and see how your app behaves?
8 |
--------------------------------------------------------------------------------
/boot.properties:
--------------------------------------------------------------------------------
1 | BOOT_CLOJURE_NAME=org.clojure/clojure
2 | BOOT_CLOJURE_VERSION=1.8.0
3 | BOOT_VERSION=2.5.5
4 | BOOT_EMIT_TARGET=no
5 |
--------------------------------------------------------------------------------
/build.boot:
--------------------------------------------------------------------------------
1 | (set-env!
2 | :source-paths #{"src"}
3 | :resource-paths #{"resources"}
4 | :dependencies '[[adzerk/boot-cljs "1.7.228-1" :scope "test"]
5 | [adzerk/boot-cljs-repl "0.3.0" :scope "test"]
6 | [adzerk/boot-reload "0.4.8" :scope "test"]
7 | [pandeiro/boot-http "0.7.2" :scope "test"]
8 | [com.cemerick/piggieback "0.2.1" :scope "test"]
9 | [org.clojure/tools.nrepl "0.2.12" :scope "test"]
10 | [weasel "0.7.0" :scope "test"]
11 |
12 | [org.clojure/clojure "1.8.0"]
13 | [org.clojure/clojurescript "1.9.225"]
14 | [org.clojure/test.check "0.9.0"]
15 |
16 | [devcards "0.2.1-7"]
17 | [sablono "0.7.1"]
18 |
19 | [cljsjs/react "15.0.2-0"]
20 | [cljsjs/react-dom "15.0.2-0"]
21 | [cljsjs/react-dom-server "15.0.2-0"]])
22 |
23 | (require
24 | '[adzerk.boot-cljs :refer [cljs]]
25 | '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]]
26 | '[adzerk.boot-reload :refer [reload]]
27 | '[pandeiro.boot-http :refer [serve]])
28 |
29 | (deftask build []
30 | (comp (speak)
31 |
32 | (cljs)
33 | ))
34 |
35 | (deftask run []
36 | (comp (serve)
37 | (watch)
38 | (cljs-repl)
39 | (reload)
40 | (build)))
41 |
42 | (deftask production []
43 | (task-options! cljs {:optimizations :advanced})
44 | identity)
45 |
46 | (deftask development []
47 | (task-options! cljs {:optimizations :none :source-map true}
48 | reload {:on-jsload 'speccards.app/init})
49 | identity)
50 |
51 | (deftask dev
52 | "Simple alias to run application in development mode"
53 | []
54 | (comp (development)
55 | (run)))
56 |
57 | (deftask prod []
58 | "Simple alias to generate a production build"
59 | []
60 | (comp (production)
61 | (build)
62 | (target :dir #{"docs"})))
63 |
--------------------------------------------------------------------------------
/docs/css/app.css:
--------------------------------------------------------------------------------
1 | button {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | background: none;
6 | font-size: 100%;
7 | vertical-align: baseline;
8 | font-family: inherit;
9 | font-weight: inherit;
10 | color: inherit;
11 | -webkit-appearance: none;
12 | appearance: none;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-font-smoothing: antialiased;
15 | font-smoothing: antialiased;
16 | }
17 |
18 | #content {
19 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
20 | line-height: 1.4em;
21 | background: #f5f5f5;
22 | color: #4d4d4d;
23 | min-width: 230px;
24 | max-width: 550px;
25 | margin: 0 auto;
26 | -webkit-font-smoothing: antialiased;
27 | -moz-font-smoothing: antialiased;
28 | font-smoothing: antialiased;
29 | font-weight: 300;
30 | }
31 |
32 | button,
33 | input[type="checkbox"] {
34 | outline: none;
35 | }
36 |
37 | .hidden {
38 | display: none;
39 | }
40 |
41 | #todoapp {
42 | background: #fff;
43 | margin: 130px 0 40px 0;
44 | position: relative;
45 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
46 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
47 | }
48 |
49 | #todoapp input::-webkit-input-placeholder {
50 | font-style: italic;
51 | font-weight: 300;
52 | color: #e6e6e6;
53 | }
54 |
55 | #todoapp input::-moz-placeholder {
56 | font-style: italic;
57 | font-weight: 300;
58 | color: #e6e6e6;
59 | }
60 |
61 | #todoapp input::input-placeholder {
62 | font-style: italic;
63 | font-weight: 300;
64 | color: #e6e6e6;
65 | }
66 |
67 | #todoapp h1 {
68 | position: absolute;
69 | top: -155px;
70 | width: 100%;
71 | font-size: 100px;
72 | font-weight: 100;
73 | text-align: center;
74 | color: rgba(175, 47, 47, 0.15);
75 | -webkit-text-rendering: optimizeLegibility;
76 | -moz-text-rendering: optimizeLegibility;
77 | text-rendering: optimizeLegibility;
78 | }
79 |
80 | #new-todo,
81 | .edit {
82 | position: relative;
83 | margin: 0;
84 | width: 100%;
85 | font-size: 24px;
86 | font-family: inherit;
87 | font-weight: inherit;
88 | line-height: 1.4em;
89 | border: 0;
90 | outline: none;
91 | color: inherit;
92 | padding: 6px;
93 | border: 1px solid #999;
94 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
95 | box-sizing: border-box;
96 | -webkit-font-smoothing: antialiased;
97 | -moz-font-smoothing: antialiased;
98 | font-smoothing: antialiased;
99 | }
100 |
101 | #new-todo {
102 | padding: 16px 16px 16px 60px;
103 | border: none;
104 | background: rgba(0, 0, 0, 0.003);
105 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
106 | }
107 |
108 | #main {
109 | position: relative;
110 | z-index: 2;
111 | border-top: 1px solid #e6e6e6;
112 | }
113 |
114 | label[for='toggle-all'] {
115 | display: none;
116 | }
117 |
118 | #toggle-all {
119 | position: absolute;
120 | top: -55px;
121 | left: -12px;
122 | width: 60px;
123 | height: 34px;
124 | text-align: center;
125 | border: none; /* Mobile Safari */
126 | }
127 |
128 | #toggle-all:before {
129 | content: '❯';
130 | font-size: 22px;
131 | color: #e6e6e6;
132 | padding: 10px 27px 10px 27px;
133 | }
134 |
135 | #toggle-all:checked:before {
136 | color: #737373;
137 | }
138 |
139 | #todo-list {
140 | margin: 0;
141 | padding: 0;
142 | list-style: none;
143 | }
144 |
145 | #todo-list li {
146 | position: relative;
147 | font-size: 24px;
148 | border-bottom: 1px solid #ededed;
149 | }
150 |
151 | #todo-list li:last-child {
152 | border-bottom: none;
153 | }
154 |
155 | #todo-list li.editing {
156 | border-bottom: none;
157 | padding: 0;
158 | }
159 |
160 | #todo-list li.editing .edit {
161 | display: block;
162 | width: 506px;
163 | padding: 13px 17px 12px 17px;
164 | margin: 0 0 0 43px;
165 | }
166 |
167 | #todo-list li.editing .view {
168 | display: none;
169 | }
170 |
171 | #todo-list li .toggle {
172 | text-align: center;
173 | width: 40px;
174 | /* auto, since non-WebKit browsers doesn't support input styling */
175 | height: auto;
176 | position: absolute;
177 | top: 0;
178 | bottom: 0;
179 | margin: auto 0;
180 | border: none; /* Mobile Safari */
181 | -webkit-appearance: none;
182 | appearance: none;
183 | }
184 |
185 | #todo-list li .toggle:after {
186 | content: url('data:image/svg+xml;utf8,');
187 | }
188 |
189 | #todo-list li .toggle:checked:after {
190 | content: url('data:image/svg+xml;utf8,');
191 | }
192 |
193 | #todo-list li label {
194 | white-space: pre-line;
195 | word-break: break-all;
196 | padding: 15px 60px 15px 15px;
197 | margin-left: 45px;
198 | display: block;
199 | line-height: 1.2;
200 | transition: color 0.4s;
201 | }
202 |
203 | #todo-list li.completed label {
204 | color: #d9d9d9;
205 | text-decoration: line-through;
206 | }
207 |
208 | #todo-list li .destroy {
209 | display: none;
210 | position: absolute;
211 | top: 0;
212 | right: 10px;
213 | bottom: 0;
214 | width: 40px;
215 | height: 40px;
216 | margin: auto 0;
217 | font-size: 30px;
218 | color: #cc9a9a;
219 | margin-bottom: 11px;
220 | transition: color 0.2s ease-out;
221 | }
222 |
223 | #todo-list li .destroy:hover {
224 | color: #af5b5e;
225 | }
226 |
227 | #todo-list li .destroy:after {
228 | content: '×';
229 | }
230 |
231 | #todo-list li:hover .destroy {
232 | display: block;
233 | }
234 |
235 | #todo-list li .edit {
236 | display: none;
237 | }
238 |
239 | #todo-list li.editing:last-child {
240 | margin-bottom: -1px;
241 | }
242 |
243 | #footer {
244 | color: #777;
245 | padding: 10px 15px;
246 | height: 20px;
247 | text-align: center;
248 | border-top: 1px solid #e6e6e6;
249 | }
250 |
251 | #footer:before {
252 | content: '';
253 | position: absolute;
254 | right: 0;
255 | bottom: 0;
256 | left: 0;
257 | height: 50px;
258 | overflow: hidden;
259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
260 | 0 8px 0 -3px #f6f6f6,
261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
262 | 0 16px 0 -6px #f6f6f6,
263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
264 | }
265 |
266 | #todo-count {
267 | float: left;
268 | text-align: left;
269 | }
270 |
271 | #todo-count strong {
272 | font-weight: 300;
273 | }
274 |
275 | #filters {
276 | margin: 0;
277 | padding: 0;
278 | list-style: none;
279 | position: absolute;
280 | right: 0;
281 | left: 0;
282 | }
283 |
284 | #filters li {
285 | display: inline;
286 | }
287 |
288 | #filters li a {
289 | color: inherit;
290 | margin: 3px;
291 | padding: 3px 7px;
292 | text-decoration: none;
293 | border: 1px solid transparent;
294 | border-radius: 3px;
295 | }
296 |
297 | #filters li a.selected,
298 | #filters li a:hover {
299 | border-color: rgba(175, 47, 47, 0.1);
300 | }
301 |
302 | #filters li a.selected {
303 | border-color: rgba(175, 47, 47, 0.2);
304 | }
305 |
306 | #clear-completed,
307 | html #clear-completed:active {
308 | float: right;
309 | position: relative;
310 | line-height: 20px;
311 | text-decoration: none;
312 | cursor: pointer;
313 | position: relative;
314 | }
315 |
316 | #clear-completed:hover {
317 | text-decoration: underline;
318 | }
319 |
320 | #info {
321 | margin: 65px auto 0;
322 | color: #bfbfbf;
323 | font-size: 10px;
324 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
325 | text-align: center;
326 | }
327 |
328 | #info p {
329 | line-height: 1;
330 | }
331 |
332 | #info a {
333 | color: inherit;
334 | text-decoration: none;
335 | font-weight: 400;
336 | }
337 |
338 | #info a:hover {
339 | text-decoration: underline;
340 | }
341 |
342 | /*
343 | Hack to remove background from Mobile Safari.
344 | Can't use it globally since it destroys checkboxes in Firefox
345 | */
346 | @media screen and (-webkit-min-device-pixel-ratio:0) {
347 | #toggle-all,
348 | #todo-list li .toggle {
349 | background: none;
350 | }
351 |
352 | #todo-list li .toggle {
353 | height: 40px;
354 | }
355 |
356 | #toggle-all {
357 | -webkit-transform: rotate(90deg);
358 | transform: rotate(90deg);
359 | -webkit-appearance: none;
360 | appearance: none;
361 | }
362 | }
363 |
364 | @media (max-width: 430px) {
365 | #footer {
366 | height: 50px;
367 | }
368 |
369 | #filters {
370 | bottom: 10px;
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | TodoMVC
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/js/app.cljs.edn:
--------------------------------------------------------------------------------
1 | {:require [speccards.app]
2 | :init-fns [speccards.app/init]
3 | :compiler-options {:asset-path "js/app.out"
4 | :devcards true}}
5 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | button {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | background: none;
6 | font-size: 100%;
7 | vertical-align: baseline;
8 | font-family: inherit;
9 | font-weight: inherit;
10 | color: inherit;
11 | -webkit-appearance: none;
12 | appearance: none;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-font-smoothing: antialiased;
15 | font-smoothing: antialiased;
16 | }
17 |
18 | #content {
19 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
20 | line-height: 1.4em;
21 | background: #f5f5f5;
22 | color: #4d4d4d;
23 | min-width: 230px;
24 | max-width: 550px;
25 | margin: 0 auto;
26 | -webkit-font-smoothing: antialiased;
27 | -moz-font-smoothing: antialiased;
28 | font-smoothing: antialiased;
29 | font-weight: 300;
30 | }
31 |
32 | button,
33 | input[type="checkbox"] {
34 | outline: none;
35 | }
36 |
37 | .hidden {
38 | display: none;
39 | }
40 |
41 | #todoapp {
42 | background: #fff;
43 | margin: 130px 0 40px 0;
44 | position: relative;
45 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
46 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
47 | }
48 |
49 | #todoapp input::-webkit-input-placeholder {
50 | font-style: italic;
51 | font-weight: 300;
52 | color: #e6e6e6;
53 | }
54 |
55 | #todoapp input::-moz-placeholder {
56 | font-style: italic;
57 | font-weight: 300;
58 | color: #e6e6e6;
59 | }
60 |
61 | #todoapp input::input-placeholder {
62 | font-style: italic;
63 | font-weight: 300;
64 | color: #e6e6e6;
65 | }
66 |
67 | #todoapp h1 {
68 | position: absolute;
69 | top: -155px;
70 | width: 100%;
71 | font-size: 100px;
72 | font-weight: 100;
73 | text-align: center;
74 | color: rgba(175, 47, 47, 0.15);
75 | -webkit-text-rendering: optimizeLegibility;
76 | -moz-text-rendering: optimizeLegibility;
77 | text-rendering: optimizeLegibility;
78 | }
79 |
80 | #new-todo,
81 | .edit {
82 | position: relative;
83 | margin: 0;
84 | width: 100%;
85 | font-size: 24px;
86 | font-family: inherit;
87 | font-weight: inherit;
88 | line-height: 1.4em;
89 | border: 0;
90 | outline: none;
91 | color: inherit;
92 | padding: 6px;
93 | border: 1px solid #999;
94 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
95 | box-sizing: border-box;
96 | -webkit-font-smoothing: antialiased;
97 | -moz-font-smoothing: antialiased;
98 | font-smoothing: antialiased;
99 | }
100 |
101 | #new-todo {
102 | padding: 16px 16px 16px 60px;
103 | border: none;
104 | background: rgba(0, 0, 0, 0.003);
105 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
106 | }
107 |
108 | #main {
109 | position: relative;
110 | z-index: 2;
111 | border-top: 1px solid #e6e6e6;
112 | }
113 |
114 | label[for='toggle-all'] {
115 | display: none;
116 | }
117 |
118 | #toggle-all {
119 | position: absolute;
120 | top: -55px;
121 | left: -12px;
122 | width: 60px;
123 | height: 34px;
124 | text-align: center;
125 | border: none; /* Mobile Safari */
126 | }
127 |
128 | #toggle-all:before {
129 | content: '❯';
130 | font-size: 22px;
131 | color: #e6e6e6;
132 | padding: 10px 27px 10px 27px;
133 | }
134 |
135 | #toggle-all:checked:before {
136 | color: #737373;
137 | }
138 |
139 | #todo-list {
140 | margin: 0;
141 | padding: 0;
142 | list-style: none;
143 | }
144 |
145 | #todo-list li {
146 | position: relative;
147 | font-size: 24px;
148 | border-bottom: 1px solid #ededed;
149 | }
150 |
151 | #todo-list li:last-child {
152 | border-bottom: none;
153 | }
154 |
155 | #todo-list li.editing {
156 | border-bottom: none;
157 | padding: 0;
158 | }
159 |
160 | #todo-list li.editing .edit {
161 | display: block;
162 | width: 506px;
163 | padding: 13px 17px 12px 17px;
164 | margin: 0 0 0 43px;
165 | }
166 |
167 | #todo-list li.editing .view {
168 | display: none;
169 | }
170 |
171 | #todo-list li .toggle {
172 | text-align: center;
173 | width: 40px;
174 | /* auto, since non-WebKit browsers doesn't support input styling */
175 | height: auto;
176 | position: absolute;
177 | top: 0;
178 | bottom: 0;
179 | margin: auto 0;
180 | border: none; /* Mobile Safari */
181 | -webkit-appearance: none;
182 | appearance: none;
183 | }
184 |
185 | #todo-list li .toggle:after {
186 | content: url('data:image/svg+xml;utf8,');
187 | }
188 |
189 | #todo-list li .toggle:checked:after {
190 | content: url('data:image/svg+xml;utf8,');
191 | }
192 |
193 | #todo-list li label {
194 | white-space: pre-line;
195 | word-break: break-all;
196 | padding: 15px 60px 15px 15px;
197 | margin-left: 45px;
198 | display: block;
199 | line-height: 1.2;
200 | transition: color 0.4s;
201 | }
202 |
203 | #todo-list li.completed label {
204 | color: #d9d9d9;
205 | text-decoration: line-through;
206 | }
207 |
208 | #todo-list li .destroy {
209 | display: none;
210 | position: absolute;
211 | top: 0;
212 | right: 10px;
213 | bottom: 0;
214 | width: 40px;
215 | height: 40px;
216 | margin: auto 0;
217 | font-size: 30px;
218 | color: #cc9a9a;
219 | margin-bottom: 11px;
220 | transition: color 0.2s ease-out;
221 | }
222 |
223 | #todo-list li .destroy:hover {
224 | color: #af5b5e;
225 | }
226 |
227 | #todo-list li .destroy:after {
228 | content: '×';
229 | }
230 |
231 | #todo-list li:hover .destroy {
232 | display: block;
233 | }
234 |
235 | #todo-list li .edit {
236 | display: none;
237 | }
238 |
239 | #todo-list li.editing:last-child {
240 | margin-bottom: -1px;
241 | }
242 |
243 | #footer {
244 | color: #777;
245 | padding: 10px 15px;
246 | height: 20px;
247 | text-align: center;
248 | border-top: 1px solid #e6e6e6;
249 | }
250 |
251 | #footer:before {
252 | content: '';
253 | position: absolute;
254 | right: 0;
255 | bottom: 0;
256 | left: 0;
257 | height: 50px;
258 | overflow: hidden;
259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
260 | 0 8px 0 -3px #f6f6f6,
261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
262 | 0 16px 0 -6px #f6f6f6,
263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
264 | }
265 |
266 | #todo-count {
267 | float: left;
268 | text-align: left;
269 | }
270 |
271 | #todo-count strong {
272 | font-weight: 300;
273 | }
274 |
275 | #filters {
276 | margin: 0;
277 | padding: 0;
278 | list-style: none;
279 | position: absolute;
280 | right: 0;
281 | left: 0;
282 | }
283 |
284 | #filters li {
285 | display: inline;
286 | }
287 |
288 | #filters li a {
289 | color: inherit;
290 | margin: 3px;
291 | padding: 3px 7px;
292 | text-decoration: none;
293 | border: 1px solid transparent;
294 | border-radius: 3px;
295 | }
296 |
297 | #filters li a.selected,
298 | #filters li a:hover {
299 | border-color: rgba(175, 47, 47, 0.1);
300 | }
301 |
302 | #filters li a.selected {
303 | border-color: rgba(175, 47, 47, 0.2);
304 | }
305 |
306 | #clear-completed,
307 | html #clear-completed:active {
308 | float: right;
309 | position: relative;
310 | line-height: 20px;
311 | text-decoration: none;
312 | cursor: pointer;
313 | position: relative;
314 | }
315 |
316 | #clear-completed:hover {
317 | text-decoration: underline;
318 | }
319 |
320 | #info {
321 | margin: 65px auto 0;
322 | color: #bfbfbf;
323 | font-size: 10px;
324 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
325 | text-align: center;
326 | }
327 |
328 | #info p {
329 | line-height: 1;
330 | }
331 |
332 | #info a {
333 | color: inherit;
334 | text-decoration: none;
335 | font-weight: 400;
336 | }
337 |
338 | #info a:hover {
339 | text-decoration: underline;
340 | }
341 |
342 | /*
343 | Hack to remove background from Mobile Safari.
344 | Can't use it globally since it destroys checkboxes in Firefox
345 | */
346 | @media screen and (-webkit-min-device-pixel-ratio:0) {
347 | #toggle-all,
348 | #todo-list li .toggle {
349 | background: none;
350 | }
351 |
352 | #todo-list li .toggle {
353 | height: 40px;
354 | }
355 |
356 | #toggle-all {
357 | -webkit-transform: rotate(90deg);
358 | transform: rotate(90deg);
359 | -webkit-appearance: none;
360 | appearance: none;
361 | }
362 | }
363 |
364 | @media (max-width: 430px) {
365 | #footer {
366 | height: 50px;
367 | }
368 |
369 | #filters {
370 | bottom: 10px;
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | TodoMVC
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/resources/js/app.cljs.edn:
--------------------------------------------------------------------------------
1 | {:require [speccards.app]
2 | :init-fns [speccards.app/init]
3 | :compiler-options {:asset-path "js/app.out"
4 | :devcards true}}
5 |
--------------------------------------------------------------------------------
/src/speccards/app.cljs:
--------------------------------------------------------------------------------
1 | (ns speccards.app
2 | (:require
3 | [devcards.core :as dc]
4 | [sablono.core :as sab :include-macros true]
5 | [cljs.spec :as s :include-macros true]
6 | [clojure.test.check.generators]
7 | [clojure.string :as str])
8 | (:require-macros
9 | [devcards.core :refer [defcard deftest]]))
10 |
11 | (enable-console-print!)
12 |
13 | (defn hidden [is-hidden]
14 | (if is-hidden
15 | {:display "none"}
16 | {}))
17 |
18 | (defn pluralize [n word]
19 | (if (== n 1)
20 | word
21 | (str word "s")))
22 |
23 | (s/def :todo/title (s/and string? (complement str/blank?)))
24 | (s/def :todo/completed boolean?)
25 | (s/def :todos/item (s/keys :req [:todo/title :todo/completed]))
26 | (s/def :todos/list (s/coll-of :todos/item))
27 | (s/def :todos/showing #{:all :active :completed})
28 | (s/def :todos/view (s/keys :req [:todos/list :todos/showing]))
29 |
30 | (defn item [{:keys [todo/title todo/completed todo/editing]}]
31 | (let [class (cond-> ""
32 | completed (str "completed ")
33 | editing (str "editing"))]
34 | (sab/html
35 | [:li {:className class}
36 | [:div.view
37 | [:input.toggle {:type "checkbox"
38 | :checked (and completed "checked")
39 | :onChange #(do %)}]
40 | [:label title]
41 | [:button.destroy]
42 | [:input.edit {:ref "editField"}]]])))
43 |
44 | (defn todos [{:keys [todos/list todos/showing]}]
45 | (let [active (count (remove :todo/completed list))
46 | completed (- (count list) active)
47 | checked? (every? :todo/completed list)]
48 | (sab/html
49 | [:div#content
50 | [:div#todoapp
51 | [:header#header
52 | [:h1 "Todos"]
53 | [:input {:ref "newField"
54 | :id "new-todo"
55 | :placeholder "What needs to be done?"
56 | :onKeyDown #(do %)}]]
57 | [:section#main {:style (hidden (empty? list))}
58 | [:input#toggle-all {:type "checkbox"
59 | :onChange #(do %)
60 | :checked checked?}]
61 | (into [:ul#todo-list]
62 | (for [todo (filter (case showing
63 | :completed :todo/completed
64 | :active (complement :todo/completed)
65 | :all identity) list)]
66 | (item todo)))]
67 | [:footer#footer {:style (hidden (empty? list))}
68 | [:span#todo-count
69 | [:strong active] (str " " (pluralize active "item") " left")]
70 | (into [:ul#filters {:className (name showing)}]
71 | (for [[x y] [["" "All"] ["active" "Active"] ["completed" "Completed"]]]
72 | [:li [:a y]]))
73 | [:button#clear-completed (str "Clear completed (" completed ")")]]]])))
74 |
75 | (doseq [ex (map first (s/exercise :todos/view))]
76 | (defcard todos-example
77 | (todos ex) ex {:inspect-data true}))
78 |
79 | (defn init []
80 | (devcards.core/start-devcard-ui!))
81 |
--------------------------------------------------------------------------------