carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// GENERATED by tools/embed.sh from shared/ and static/. Do not edit.
2
3enum Assets {
4 static let baseCSS = #"""
5hr {
6 margin: 20px 0;
7 border: 0;
8 border-top: 1px dashed #c5c5c5;
9 border-bottom: 1px dashed #f7f7f7;
10}
11
12.learn a {
13 font-weight: normal;
14 text-decoration: none;
15 color: #b83f45;
16}
17
18.learn a:hover {
19 text-decoration: underline;
20 color: #787e7e;
21}
22
23.learn h3,
24.learn h4,
25.learn h5 {
26 margin: 10px 0;
27 font-weight: 500;
28 line-height: 1.2;
29 color: #000;
30}
31
32.learn h3 {
33 font-size: 24px;
34}
35
36.learn h4 {
37 font-size: 18px;
38}
39
40.learn h5 {
41 margin-bottom: 0;
42 font-size: 14px;
43}
44
45.learn ul {
46 padding: 0;
47 margin: 0 0 30px 25px;
48}
49
50.learn li {
51 line-height: 20px;
52}
53
54.learn p {
55 font-size: 15px;
56 font-weight: 300;
57 line-height: 1.3;
58 margin-top: 0;
59 margin-bottom: 0;
60}
61
62#issue-count {
63 display: none;
64}
65
66.quote {
67 border: none;
68 margin: 20px 0 60px 0;
69}
70
71.quote p {
72 font-style: italic;
73}
74
75.quote p:before {
76 content: '“';
77 font-size: 50px;
78 opacity: .15;
79 position: absolute;
80 top: -20px;
81 left: 3px;
82}
83
84.quote p:after {
85 content: '”';
86 font-size: 50px;
87 opacity: .15;
88 position: absolute;
89 bottom: -42px;
90 right: 3px;
91}
92
93.quote footer {
94 position: absolute;
95 bottom: -40px;
96 right: 0;
97}
98
99.quote footer img {
100 border-radius: 3px;
101}
102
103.quote footer a {
104 margin-left: 5px;
105 vertical-align: middle;
106}
107
108.speech-bubble {
109 position: relative;
110 padding: 10px;
111 background: rgba(0, 0, 0, .04);
112 border-radius: 5px;
113}
114
115.speech-bubble:after {
116 content: '';
117 position: absolute;
118 top: 100%;
119 right: 30px;
120 border: 13px solid transparent;
121 border-top-color: rgba(0, 0, 0, .04);
122}
123
124.learn-bar > .learn {
125 position: absolute;
126 width: 272px;
127 top: 8px;
128 left: -300px;
129 padding: 10px;
130 border-radius: 5px;
131 background-color: rgba(255, 255, 255, .6);
132 transition-property: left;
133 transition-duration: 500ms;
134}
135
136@media (min-width: 899px) {
137 .learn-bar {
138 width: auto;
139 padding-left: 300px;
140 }
141
142 .learn-bar > .learn {
143 left: 8px;
144 }
145}
146
147"""#
148
149 static let indexCSS = #"""
150@charset 'utf-8';
151
152html,
153body {
154 margin: 0;
155 padding: 0;
156}
157
158button {
159 margin: 0;
160 padding: 0;
161 border: 0;
162 background: none;
163 font-size: 100%;
164 vertical-align: baseline;
165 font-family: inherit;
166 font-weight: inherit;
167 color: inherit;
168 -webkit-appearance: none;
169 appearance: none;
170 -webkit-font-smoothing: antialiased;
171 -moz-osx-font-smoothing: grayscale;
172}
173
174body {
175 font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
176 line-height: 1.4em;
177 background: #f5f5f5;
178 color: #111111;
179 min-width: 230px;
180 max-width: 550px;
181 margin: 0 auto;
182 -webkit-font-smoothing: antialiased;
183 -moz-osx-font-smoothing: grayscale;
184 font-weight: 300;
185}
186
187.hidden {
188 display: none;
189}
190
191.todoapp {
192 background: #fff;
193 margin: 130px 0 40px 0;
194 position: relative;
195 box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
196 0 25px 50px 0 rgba(0, 0, 0, 0.1);
197}
198
199.todoapp input::-webkit-input-placeholder {
200 font-style: italic;
201 font-weight: 400;
202 color: rgba(0, 0, 0, 0.4);
203}
204
205.todoapp input::-moz-placeholder {
206 font-style: italic;
207 font-weight: 400;
208 color: rgba(0, 0, 0, 0.4);
209}
210
211.todoapp input::input-placeholder {
212 font-style: italic;
213 font-weight: 400;
214 color: rgba(0, 0, 0, 0.4);
215}
216
217.todoapp h1 {
218 position: absolute;
219 top: -140px;
220 width: 100%;
221 font-size: 80px;
222 font-weight: 200;
223 text-align: center;
224 color: #b83f45;
225 -webkit-text-rendering: optimizeLegibility;
226 -moz-text-rendering: optimizeLegibility;
227 text-rendering: optimizeLegibility;
228}
229
230.new-todo,
231.edit {
232 position: relative;
233 margin: 0;
234 width: 100%;
235 font-size: 24px;
236 font-family: inherit;
237 font-weight: inherit;
238 line-height: 1.4em;
239 color: inherit;
240 padding: 6px;
241 border: 1px solid #999;
242 box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
243 box-sizing: border-box;
244 -webkit-font-smoothing: antialiased;
245 -moz-osx-font-smoothing: grayscale;
246}
247
248.new-todo {
249 padding: 16px 16px 16px 60px;
250 height: 65px;
251 border: none;
252 background: rgba(0, 0, 0, 0.003);
253 box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
254}
255
256.main {
257 position: relative;
258 z-index: 2;
259 border-top: 1px solid #e6e6e6;
260}
261
262.toggle-all {
263 width: 1px;
264 height: 1px;
265 border: none; /* Mobile Safari */
266 opacity: 0;
267 position: absolute;
268 right: 100%;
269 bottom: 100%;
270}
271
272.toggle-all + label {
273 display: flex;
274 align-items: center;
275 justify-content: center;
276 width: 45px;
277 height: 65px;
278 font-size: 0;
279 position: absolute;
280 top: -65px;
281 left: -0;
282}
283
284.toggle-all + label:before {
285 content: '❯';
286 display: inline-block;
287 font-size: 22px;
288 color: #949494;
289 padding: 10px 27px 10px 27px;
290 -webkit-transform: rotate(90deg);
291 transform: rotate(90deg);
292}
293
294.toggle-all:checked + label:before {
295 color: #484848;
296}
297
298.todo-list {
299 margin: 0;
300 padding: 0;
301 list-style: none;
302}
303
304.todo-list li {
305 position: relative;
306 font-size: 24px;
307 border-bottom: 1px solid #ededed;
308}
309
310.todo-list li:last-child {
311 border-bottom: none;
312}
313
314.todo-list li.editing {
315 border-bottom: none;
316 padding: 0;
317}
318
319.todo-list li.editing .edit {
320 display: block;
321 width: calc(100% - 43px);
322 padding: 12px 16px;
323 margin: 0 0 0 43px;
324}
325
326.todo-list li.editing .view {
327 display: none;
328}
329
330.todo-list li .toggle {
331 text-align: center;
332 width: 40px;
333 /* auto, since non-WebKit browsers doesn't support input styling */
334 height: auto;
335 position: absolute;
336 top: 0;
337 bottom: 0;
338 margin: auto 0;
339 border: none; /* Mobile Safari */
340 -webkit-appearance: none;
341 appearance: none;
342}
343
344.todo-list li .toggle {
345 opacity: 0;
346}
347
348.todo-list li .toggle + label {
349 /*
350 Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
351 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
352 */
353 background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
354 background-repeat: no-repeat;
355 background-position: center left;
356}
357
358.todo-list li .toggle:checked + label {
359 background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
360}
361
362.todo-list li label {
363 overflow-wrap: break-word;
364 padding: 15px 15px 15px 60px;
365 display: block;
366 line-height: 1.2;
367 transition: color 0.4s;
368 font-weight: 400;
369 color: #484848;
370}
371
372.todo-list li.completed label {
373 color: #949494;
374 text-decoration: line-through;
375}
376
377.todo-list li .destroy {
378 display: none;
379 position: absolute;
380 top: 0;
381 right: 10px;
382 bottom: 0;
383 width: 40px;
384 height: 40px;
385 margin: auto 0;
386 font-size: 30px;
387 color: #949494;
388 transition: color 0.2s ease-out;
389}
390
391.todo-list li .destroy:hover,
392.todo-list li .destroy:focus {
393 color: #C18585;
394}
395
396.todo-list li .destroy:after {
397 content: '×';
398 display: block;
399 height: 100%;
400 line-height: 1.1;
401}
402
403.todo-list li:hover .destroy {
404 display: block;
405}
406
407.todo-list li .edit {
408 display: none;
409}
410
411.todo-list li.editing:last-child {
412 margin-bottom: -1px;
413}
414
415.footer {
416 padding: 10px 15px;
417 height: 20px;
418 text-align: center;
419 font-size: 15px;
420 border-top: 1px solid #e6e6e6;
421}
422
423.footer:before {
424 content: '';
425 position: absolute;
426 right: 0;
427 bottom: 0;
428 left: 0;
429 height: 50px;
430 overflow: hidden;
431 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
432 0 8px 0 -3px #f6f6f6,
433 0 9px 1px -3px rgba(0, 0, 0, 0.2),
434 0 16px 0 -6px #f6f6f6,
435 0 17px 2px -6px rgba(0, 0, 0, 0.2);
436}
437
438.todo-count {
439 float: left;
440 text-align: left;
441}
442
443.todo-count strong {
444 font-weight: 300;
445}
446
447.filters {
448 margin: 0;
449 padding: 0;
450 list-style: none;
451 position: absolute;
452 right: 0;
453 left: 0;
454}
455
456.filters li {
457 display: inline;
458}
459
460.filters li a {
461 color: inherit;
462 margin: 3px;
463 padding: 3px 7px;
464 text-decoration: none;
465 border: 1px solid transparent;
466 border-radius: 3px;
467}
468
469.filters li a:hover {
470 border-color: #DB7676;
471}
472
473.filters li a.selected {
474 border-color: #CE4646;
475}
476
477.clear-completed,
478html .clear-completed:active {
479 float: right;
480 position: relative;
481 line-height: 19px;
482 text-decoration: none;
483 cursor: pointer;
484}
485
486.clear-completed:hover {
487 text-decoration: underline;
488}
489
490.info {
491 margin: 65px auto 0;
492 color: #4d4d4d;
493 font-size: 11px;
494 text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
495 text-align: center;
496}
497
498.info p {
499 line-height: 1;
500}
501
502.info a {
503 color: inherit;
504 text-decoration: none;
505 font-weight: 400;
506}
507
508.info a:hover {
509 text-decoration: underline;
510}
511
512/*
513 Hack to remove background from Mobile Safari.
514 Can't use it globally since it destroys checkboxes in Firefox
515*/
516@media screen and (-webkit-min-device-pixel-ratio:0) {
517 .toggle-all,
518 .todo-list li .toggle {
519 background: none;
520 }
521
522 .todo-list li .toggle {
523 height: 40px;
524 }
525}
526
527@media (max-width: 430px) {
528 .footer {
529 height: 50px;
530 }
531
532 .filters {
533 bottom: 10px;
534 }
535}
536
537:focus,
538.toggle:focus + label,
539.toggle-all:focus + label {
540 box-shadow: 0 0 2px 2px #CF7D7D;
541 outline: 0;
542}
543
544"""#
545
546 static let appCSS = #"""
547/* The controls a page without JavaScript uses. app.js's inline starter puts
548 the class "js" on <html>, and then they're hidden: the checkboxes,
549 double-click and blur take over, exactly as in TodoMVC. */
550.js .nojs {
551 display: none !important;
552}
553
554/* Forms that only wrap a control take no part in layout. */
555form.inline {
556 display: contents;
557}
558
559/* Without JavaScript, a submit button lies exactly over each checkbox, so
560 the page looks the same either way. */
561.todo-list li .view > button[data-action="toggle"] {
562 position: absolute;
563 top: 0;
564 bottom: 0;
565 left: 0;
566 width: 40px;
567 height: 40px;
568 margin: auto 0;
569 font-size: 0;
570 cursor: pointer;
571 z-index: 1;
572}
573
574.main button[data-action="toggle-all"] {
575 position: absolute;
576 top: -65px;
577 left: 0;
578 width: 45px;
579 height: 65px;
580 font-size: 0;
581 cursor: pointer;
582 z-index: 1;
583}
584
585/* Without JavaScript there's no double-click, so each item has an edit link
586 beside the delete button. */
587.todo-list li .view > a[data-action="edit"] {
588 position: absolute;
589 top: 0;
590 bottom: 0;
591 right: 55px;
592 height: 40px;
593 margin: auto 0;
594 font-size: 20px;
595 line-height: 40px;
596 color: #949494;
597 text-decoration: none;
598}
599
600.todo-list li .view > a[data-action="edit"]:hover,
601.todo-list li .view > a[data-action="edit"]:focus {
602 color: #484848;
603}
604
605html:not(.js) .todo-list li label {
606 padding-right: 90px;
607}
608
609"""#
610
611 static let appJS = #"""
612// TodoMVC behaviour on top of the server-rendered page. Every change is
613// still the page's own form post; this sends it with fetch and swaps in
614// the new list instead of loading the page again.
615
616const app = document.querySelector('.todoapp');
617
618// Actions run one at a time, in the order they were made, so each answer
619// is the newest state when it's swapped in and an older one can never
620// overwrite it.
621let queue = Promise.resolve();
622
623// Sends a form and replaces section.main and footer.footer with what the
624// server answers. The form is read when it's submitted, so the visitor can
625// carry on typing. Anything unexpected falls back to a full page load.
626function send(form) {
627 const action = form.action;
628 const body = new URLSearchParams(new FormData(form));
629 queue = queue.then(async () => {
630 const res = await fetch(action, {
631 method: 'POST',
632 headers: { 'X-Todo-Partial': '1' },
633 body,
634 });
635 if (!res.ok) throw new Error(`${res.status}`);
636 swap(await res.text());
637 }).catch(() => location.reload());
638}
639
640function swap(html) {
641 const tpl = document.createElement('template');
642 tpl.innerHTML = html;
643 app.querySelectorAll(':scope > .main, :scope > .footer').forEach((el) => el.remove());
644 app.append(tpl.content);
645}
646
647app.addEventListener('submit', (e) => {
648 e.preventDefault();
649 const form = e.target;
650 send(form);
651 // The new-todo input is ready for the next title straight away.
652 if (form.closest('.header')) form.reset();
653 // An edit is saved once: the blur that follows doesn't save it again.
654 const li = form.closest('.todo-list li');
655 if (li) li.dataset.saved = '1';
656});
657
658app.addEventListener('change', (e) => {
659 const li = e.target.closest('li[data-id]');
660 if (e.target.matches('.toggle') && li) send(document.getElementById(`toggle-${li.dataset.id}`));
661 if (e.target.matches('.toggle-all')) send(app.querySelector('[data-action="toggle-all"]').form);
662});
663
664// Editing: double-click opens the editor, Enter or blur saves, Escape
665// cancels. An empty title deletes the item (the server decides that).
666function startEditing(li) {
667 li.classList.add('editing');
668 const input = li.querySelector('.edit');
669 input.focus();
670 input.setSelectionRange(input.value.length, input.value.length);
671}
672
673app.addEventListener('dblclick', (e) => {
674 const li = e.target.closest('.todo-list li');
675 if (li && e.target.matches('.view label')) startEditing(li);
676});
677
678app.addEventListener('keydown', (e) => {
679 if (e.key !== 'Escape' || !e.target.matches('.edit')) return;
680 const input = e.target;
681 input.value = input.defaultValue;
682 input.closest('li').classList.remove('editing');
683 input.blur();
684});
685
686app.addEventListener('focusout', (e) => {
687 if (!e.target.matches('.edit')) return;
688 const li = e.target.closest('li');
689 if (!li.classList.contains('editing') || li.dataset.saved) return;
690 li.dataset.saved = '1';
691 send(e.target.form);
692});
693
694// The server's edit view (?edit=<id>) opens with that item's editor; put
695// the cursor at the end, as double-click does.
696const editing = app.querySelector('.todo-list li.editing');
697if (editing) startEditing(editing);
698
699"""#
700
701}
702