easyfaa 0.20.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -468
- data/README.md +49 -222
- data/app/assets/fonts/easy/outfit-latin-ext.woff2 +0 -0
- data/app/assets/fonts/easy/outfit-latin.woff2 +0 -0
- data/app/assets/images/easy/favicon.ico +0 -0
- data/app/assets/images/easy/favicon.svg +11 -0
- data/app/assets/images/easy/sprite.svg +52 -0
- data/app/assets/javascripts/easy/add_rows.js +38 -0
- data/app/assets/javascripts/easy/flyer.js +100 -0
- data/app/assets/javascripts/easy/highlight.js +54 -0
- data/app/assets/javascripts/easy/konami.js +73 -0
- data/app/assets/javascripts/easy/modal.js +49 -0
- data/app/assets/javascripts/easy/pin_code.js +43 -0
- data/app/assets/javascripts/easy/report_sort.js +57 -0
- data/app/assets/javascripts/easy/rotating_placeholder.js +35 -0
- data/app/assets/javascripts/easy/step_foot.js +17 -0
- data/app/assets/javascripts/easy/switches.js +96 -0
- data/app/assets/javascripts/easy/switches_address.js +39 -0
- data/app/assets/javascripts/easy/tabs.js +35 -0
- data/app/assets/javascripts/easy/theme.js +41 -0
- data/app/assets/stylesheets/easy/base.css +42 -0
- data/app/assets/stylesheets/easy/components.css +533 -0
- data/app/assets/stylesheets/easy/layout.css +47 -0
- data/app/assets/stylesheets/easy/tokens.css +11 -0
- data/app/controllers/easyfaa/errors_controller.rb +41 -0
- data/app/helpers/easyfaa/easy_helper.rb +58 -0
- data/app/models/aircraft_avatar.rb +61 -0
- data/app/views/easy/_ad_card.html.erb +16 -0
- data/app/views/easy/_ad_list.html.erb +11 -0
- data/app/views/easy/_flyer.html.erb +10 -0
- data/app/views/easy/_logo.html.erb +15 -0
- data/app/views/easy/_modal.html.erb +17 -0
- data/app/views/easy/_part_tabs.html.erb +24 -0
- data/app/views/easy/_pin_code.html.erb +10 -0
- data/app/views/easy/_report_head.html.erb +28 -0
- data/app/views/easy/mailer/_button.html.erb +10 -0
- data/app/views/easy/mailer/_card.html.erb +23 -0
- data/app/views/easy/mailer/_code.html.erb +28 -0
- data/app/views/easy/mailer/_code.text.erb +19 -0
- data/app/views/easy/mailer/_frame.html.erb +35 -0
- data/app/views/easy/mailer/_text.html.erb +7 -0
- data/app/views/easyfaa/errors/show.html.erb +17 -0
- data/app/views/layouts/easy/site.html.erb +55 -0
- data/app/views/layouts/easyfaa/errors.html.erb +30 -0
- data/config/aircraft_avatars.yml +26 -0
- data/lib/easyfaa/engine.rb +16 -43
- data/lib/easyfaa/version.rb +2 -2
- data/lib/easyfaa.rb +14 -35
- metadata +52 -181
- data/app/views/layouts/easyfaa.html.erb +0 -24
- data/app/views/recourses/_head.html.erb +0 -3
- data/art/autopilot.svg +0 -9
- data/art/cube.svg +0 -9
- data/art/design.svg +0 -8
- data/art/down.html.erb +0 -95
- data/art/easyfaa.svg +0 -7
- data/art/fountain.svg +0 -9
- data/art/goldrush.svg +0 -7
- data/art/houston.svg +0 -8
- data/art/icons.txt +0 -185
- data/art/index.html.erb +0 -157
- data/config/locales/easyfaa.en.yml +0 -7
- data/lib/easyfaa/copy.rb +0 -5
- data/lib/easyfaa/down.rb +0 -19
- data/lib/easyfaa/helpers/flows.rb +0 -53
- data/lib/easyfaa/helpers/heads.rb +0 -60
- data/lib/easyfaa/helpers/logos.rb +0 -43
- data/lib/easyfaa/helpers.rb +0 -11
- data/lib/easyfaa/marks.rb +0 -16
- data/public/404.html +0 -118
- data/public/_headers +0 -28
- data/public/bh/css/bh.css +0 -1
- data/public/bh/js/bh.js +0 -152
- data/public/bh/theme/bootstrap.css +0 -10
- data/public/bh/theme/dawn.css +0 -189
- data/public/bh/theme/dracula.css +0 -189
- data/public/bh/theme/gruvbox.css +0 -189
- data/public/bh/theme/monokai.css +0 -190
- data/public/bh/theme/nord.css +0 -189
- data/public/bh/theme/one_dark.css +0 -189
- data/public/bh/theme/solarized.css +0 -188
- data/public/bh/theme/tokyo_night.css +0 -189
- data/public/css/easyfaa.css +0 -1
- data/public/down/autopilot.html +0 -87
- data/public/down/cube.html +0 -87
- data/public/down/design.html +0 -87
- data/public/down/easyfaa.html +0 -84
- data/public/down/fountain.html +0 -87
- data/public/down/goldrush.html +0 -87
- data/public/down/houston.html +0 -87
- data/public/favicon/autopilot-180.png +0 -0
- data/public/favicon/autopilot-192.png +0 -0
- data/public/favicon/autopilot-512.png +0 -0
- data/public/favicon/autopilot-96.png +0 -0
- data/public/favicon/autopilot-dev.svg +0 -9
- data/public/favicon/autopilot.ico +0 -0
- data/public/favicon/autopilot.svg +0 -9
- data/public/favicon/autopilot.webmanifest +0 -12
- data/public/favicon/cube-180.png +0 -0
- data/public/favicon/cube-192.png +0 -0
- data/public/favicon/cube-512.png +0 -0
- data/public/favicon/cube-96.png +0 -0
- data/public/favicon/cube-dev.svg +0 -9
- data/public/favicon/cube.ico +0 -0
- data/public/favicon/cube.svg +0 -9
- data/public/favicon/cube.webmanifest +0 -12
- data/public/favicon/design-180.png +0 -0
- data/public/favicon/design-192.png +0 -0
- data/public/favicon/design-512.png +0 -0
- data/public/favicon/design-96.png +0 -0
- data/public/favicon/design-dev.svg +0 -8
- data/public/favicon/design.ico +0 -0
- data/public/favicon/design.svg +0 -8
- data/public/favicon/design.webmanifest +0 -12
- data/public/favicon/easyfaa-180.png +0 -0
- data/public/favicon/easyfaa-192.png +0 -0
- data/public/favicon/easyfaa-512.png +0 -0
- data/public/favicon/easyfaa-96.png +0 -0
- data/public/favicon/easyfaa-dev.svg +0 -7
- data/public/favicon/easyfaa.ico +0 -0
- data/public/favicon/easyfaa.svg +0 -7
- data/public/favicon/easyfaa.webmanifest +0 -12
- data/public/favicon/fountain-180.png +0 -0
- data/public/favicon/fountain-192.png +0 -0
- data/public/favicon/fountain-512.png +0 -0
- data/public/favicon/fountain-96.png +0 -0
- data/public/favicon/fountain-dev.svg +0 -9
- data/public/favicon/fountain.ico +0 -0
- data/public/favicon/fountain.svg +0 -9
- data/public/favicon/fountain.webmanifest +0 -12
- data/public/favicon/goldrush-180.png +0 -0
- data/public/favicon/goldrush-192.png +0 -0
- data/public/favicon/goldrush-512.png +0 -0
- data/public/favicon/goldrush-96.png +0 -0
- data/public/favicon/goldrush-dev.svg +0 -7
- data/public/favicon/goldrush.ico +0 -0
- data/public/favicon/goldrush.svg +0 -7
- data/public/favicon/goldrush.webmanifest +0 -12
- data/public/favicon/houston-180.png +0 -0
- data/public/favicon/houston-192.png +0 -0
- data/public/favicon/houston-512.png +0 -0
- data/public/favicon/houston-96.png +0 -0
- data/public/favicon/houston-dev.svg +0 -8
- data/public/favicon/houston.ico +0 -0
- data/public/favicon/houston.svg +0 -8
- data/public/favicon/houston.webmanifest +0 -12
- data/public/favicon.ico +0 -0
- data/public/font/bootstrap-icons-6LILWPE2.woff2 +0 -0
- data/public/font/bootstrap-icons-FRBPO3H7.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-latin-LA7QXWWL.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-latin-ext-X4QHZICY.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-latin-ext.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-latin.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-vietnamese-24B6BSND.woff2 +0 -0
- data/public/font/ibm-plex-sans-500-vietnamese.woff2 +0 -0
- data/public/index.html +0 -165
- data/public/js/easyfaa.js +0 -1
- data/public/logo/autopilot.png +0 -0
- data/public/logo/autopilot.svg +0 -9
- data/public/logo/cube.png +0 -0
- data/public/logo/cube.svg +0 -9
- data/public/logo/design.png +0 -0
- data/public/logo/design.svg +0 -8
- data/public/logo/easyfaa.png +0 -0
- data/public/logo/easyfaa.svg +0 -7
- data/public/logo/fountain.png +0 -0
- data/public/logo/fountain.svg +0 -9
- data/public/logo/goldrush.png +0 -0
- data/public/logo/goldrush.svg +0 -7
- data/public/logo/houston.png +0 -0
- data/public/logo/houston.svg +0 -8
- data/public/page/400.html +0 -118
- data/public/page/404.html +0 -118
- data/public/page/406-unsupported-browser.html +0 -118
- data/public/page/422.html +0 -118
- data/public/page/500.html +0 -118
- data/public/site.webmanifest +0 -12
- data/public/theme/bootstrap.css +0 -10
- data/public/theme/dawn.css +0 -189
- data/public/theme/dracula.css +0 -189
- data/public/theme/gruvbox.css +0 -189
- data/public/theme/monokai.css +0 -190
- data/public/theme/nord.css +0 -189
- data/public/theme/one_dark.css +0 -189
- data/public/theme/solarized.css +0 -188
- data/public/theme/tokyo_night.css +0 -189
- data/public/v0.20.0/bh/css/bh.css +0 -1
- data/public/v0.20.0/bh/js/bh.js +0 -152
- data/public/v0.20.0/bh/theme/bootstrap.css +0 -10
- data/public/v0.20.0/bh/theme/dawn.css +0 -189
- data/public/v0.20.0/bh/theme/dracula.css +0 -189
- data/public/v0.20.0/bh/theme/gruvbox.css +0 -189
- data/public/v0.20.0/bh/theme/monokai.css +0 -190
- data/public/v0.20.0/bh/theme/nord.css +0 -189
- data/public/v0.20.0/bh/theme/one_dark.css +0 -189
- data/public/v0.20.0/bh/theme/solarized.css +0 -188
- data/public/v0.20.0/bh/theme/tokyo_night.css +0 -189
- data/public/v0.20.0/css/easyfaa.css +0 -1
- data/public/v0.20.0/font/bootstrap-icons-FRBPO3H7.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-latin-LA7QXWWL.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-latin-ext-X4QHZICY.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-latin-ext.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-latin.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-vietnamese-24B6BSND.woff2 +0 -0
- data/public/v0.20.0/font/ibm-plex-sans-500-vietnamese.woff2 +0 -0
- data/public/v0.20.0/js/easyfaa.js +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Marks the words of a search in the rows it leaves (easy/switches calls it at each draw): every place a word is
|
|
2
|
+
// found in a row's text is wrapped in mark.search-hit, and the marks of the search before are taken out first. Words
|
|
3
|
+
// are found within one piece of text, never across two elements, and never in a form: what it asks is the
|
|
4
|
+
// same in every row, and what is typed there is the reader's.
|
|
5
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
6
|
+
(function () {
|
|
7
|
+
var SKIP = /^(FORM|SCRIPT|STYLE)$/;
|
|
8
|
+
|
|
9
|
+
// Straight in a flex or grid box each piece of text is laid out apart, losing the space beside it: the text is
|
|
10
|
+
// first put whole in a span, one piece as it was, and marked inside it.
|
|
11
|
+
function texts(node, found) {
|
|
12
|
+
var laid = /flex|grid/.test(window.getComputedStyle(node).display);
|
|
13
|
+
for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
14
|
+
if (child.nodeType === 3 && laid && /\S/.test(child.nodeValue)) {
|
|
15
|
+
var whole = document.createElement('span');
|
|
16
|
+
node.replaceChild(whole, child);
|
|
17
|
+
whole.appendChild(child);
|
|
18
|
+
found.push(child);
|
|
19
|
+
child = whole;
|
|
20
|
+
} else if (child.nodeType === 3) { found.push(child); } else if (child.nodeType === 1 && !SKIP.test(child.nodeName)) { texts(child, found); }
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Wraps the first word found in the text, then goes on in what follows it.
|
|
26
|
+
function mark(text, words) {
|
|
27
|
+
var lower = text.nodeValue.toLowerCase(), at = -1, size = 0;
|
|
28
|
+
for (var w = 0; w < words.length; w++) {
|
|
29
|
+
var index = lower.indexOf(words[w]);
|
|
30
|
+
if (index >= 0 && (at < 0 || index < at)) { at = index; size = words[w].length; }
|
|
31
|
+
}
|
|
32
|
+
if (at < 0) { return; }
|
|
33
|
+
var hit = text.splitText(at), rest = hit.splitText(size), wrap = document.createElement('mark');
|
|
34
|
+
wrap.className = 'search-hit';
|
|
35
|
+
hit.parentNode.replaceChild(wrap, hit);
|
|
36
|
+
wrap.appendChild(hit);
|
|
37
|
+
mark(rest, words);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
window.easyHighlight = function (box, rows, words) {
|
|
41
|
+
var marks = box.querySelectorAll('mark.search-hit'), typed = [];
|
|
42
|
+
for (var m = 0; m < marks.length; m++) {
|
|
43
|
+
var parent = marks[m].parentNode;
|
|
44
|
+
parent.replaceChild(document.createTextNode(marks[m].textContent), marks[m]);
|
|
45
|
+
parent.normalize();
|
|
46
|
+
}
|
|
47
|
+
for (var w = 0; w < words.length; w++) { if (words[w]) { typed.push(words[w]); } }
|
|
48
|
+
if (!typed.length) { return; }
|
|
49
|
+
for (var r = 0; r < rows.length; r++) {
|
|
50
|
+
var found = texts(rows[r], []);
|
|
51
|
+
for (var t = 0; t < found.length; t++) { mark(found[t], typed); }
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The easter egg of render "easy/flyer": the Konami code sets the plane free, ↑ ↑ ↓ ↓ ← → ← → B A on a keyboard,
|
|
2
|
+
// or on a phone swipes up, up, down, down, left, right, left, right and two taps. It leaves its wave, wanders the
|
|
3
|
+
// window at random until it flies out of it, and never comes back in this browser (localStorage, per site).
|
|
4
|
+
// Included with easy/flyer; where motion is turned off there is no plane, and nothing to free.
|
|
5
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
6
|
+
(function () {
|
|
7
|
+
if (!document.querySelectorAll || !document.addEventListener || !window.requestAnimationFrame) { return; }
|
|
8
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return; }
|
|
9
|
+
var GONE = 'easy-plane-gone', CODES = ['UUDDLRLRBA', 'UUDDLRLRTT'];
|
|
10
|
+
var KEYS = { 38: 'U', 40: 'D', 37: 'L', 39: 'R', 66: 'B', 65: 'A' };
|
|
11
|
+
var skies = document.querySelectorAll('.js-sky'), typed = '', start = null, freed = false;
|
|
12
|
+
|
|
13
|
+
// A sky gone keeps no plane, and its trail fades; easy/flyer stops drawing it.
|
|
14
|
+
function gone() { for (var i = 0; i < skies.length; i++) { skies[i].className += ' is-gone'; } }
|
|
15
|
+
try { if (window.localStorage.getItem(GONE)) { gone(); return; } } catch (err) { /* no storage: it comes back */ }
|
|
16
|
+
|
|
17
|
+
// The plane, out of its sky and over the window, where it was and heading as it was: at 160px a second, it turns
|
|
18
|
+
// one way then the other, never sharper than a loop in three seconds, until it is past an edge.
|
|
19
|
+
function fly(flyer) {
|
|
20
|
+
var box = flyer.getBoundingClientRect(), plane = flyer.cloneNode(true), size = 34;
|
|
21
|
+
var rotation = /rotate\((-?[\d.]+)deg\)/.exec(flyer.style.transform || '');
|
|
22
|
+
var x = box.left + box.width / 2 - size / 2, y = box.top + box.height / 2 - size / 2;
|
|
23
|
+
var heading = rotation ? rotation[1] * Math.PI / 180 : 0, turn = 0, last = null;
|
|
24
|
+
if (!box.width) { return; }
|
|
25
|
+
function place() {
|
|
26
|
+
var turn = (heading * 180 / Math.PI).toFixed(1);
|
|
27
|
+
var move = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px) rotate(' + turn + 'deg)';
|
|
28
|
+
plane.style.webkitTransform = move; plane.style.transform = move;
|
|
29
|
+
}
|
|
30
|
+
plane.className = 'flyer is-free';
|
|
31
|
+
place();
|
|
32
|
+
document.body.appendChild(plane);
|
|
33
|
+
function frame(now) {
|
|
34
|
+
var dt = last === null ? 16 : Math.min(now - last, 50);
|
|
35
|
+
last = now;
|
|
36
|
+
turn = Math.max(-0.002, Math.min(0.002, turn + (Math.random() - 0.5) * 0.00002 * dt));
|
|
37
|
+
heading += turn * dt;
|
|
38
|
+
x += Math.cos(heading) * 0.16 * dt; y += Math.sin(heading) * 0.16 * dt;
|
|
39
|
+
place();
|
|
40
|
+
if (x < -size || y < -size || x > window.innerWidth || y > window.innerHeight) {
|
|
41
|
+
return document.body.removeChild(plane);
|
|
42
|
+
}
|
|
43
|
+
window.requestAnimationFrame(frame);
|
|
44
|
+
}
|
|
45
|
+
window.requestAnimationFrame(frame);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function free() {
|
|
49
|
+
try { window.localStorage.setItem(GONE, '1'); } catch (err) { /* no storage: it comes back */ }
|
|
50
|
+
for (var i = 0; i < skies.length; i++) { fly(skies[i].querySelector('.js-flyer')); }
|
|
51
|
+
gone();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function push(symbol) {
|
|
55
|
+
typed = (typed + symbol).slice(-10);
|
|
56
|
+
if (!freed && (typed === CODES[0] || typed === CODES[1])) { freed = true; free(); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
document.addEventListener('keydown', function (event) { push(KEYS[event.keyCode] || '-'); });
|
|
60
|
+
// A swipe is its longer way, 30px at least; a touch that hardly moved is a tap.
|
|
61
|
+
document.addEventListener('touchstart', function (event) {
|
|
62
|
+
start = event.touches.length === 1 ? { x: event.touches[0].clientX, y: event.touches[0].clientY } : null;
|
|
63
|
+
}, { passive: true });
|
|
64
|
+
document.addEventListener('touchend', function (event) {
|
|
65
|
+
if (!start) { return; }
|
|
66
|
+
var dx = event.changedTouches[0].clientX - start.x, dy = event.changedTouches[0].clientY - start.y;
|
|
67
|
+
var far = Math.max(Math.abs(dx), Math.abs(dy));
|
|
68
|
+
if (far < 10) { push('T'); } else if (far < 30) { push('-'); } else if (Math.abs(dx) > Math.abs(dy)) {
|
|
69
|
+
push(dx > 0 ? 'R' : 'L');
|
|
70
|
+
} else { push(dy > 0 ? 'D' : 'U'); }
|
|
71
|
+
start = null;
|
|
72
|
+
}, { passive: true });
|
|
73
|
+
})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Every dialog of render "easy/modal" lets its reader out: its ×, Esc or a click around it closes it where it is,
|
|
2
|
+
// without loading a page, and the address becomes the × link's (the page without what opened it), or loses the #id
|
|
3
|
+
// that opened it. A link to that #id opens it again. Without the script, the × is a plain link.
|
|
4
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
5
|
+
(function () {
|
|
6
|
+
if (!document.querySelector || !document.addEventListener) { return; }
|
|
7
|
+
|
|
8
|
+
function close(modal) {
|
|
9
|
+
var link = modal.querySelector('.modal-x'), href = link && link.getAttribute('href');
|
|
10
|
+
modal.className = modal.className.replace(/\s*\bis-(open|closed)\b/g, '') + ' is-closed';
|
|
11
|
+
if (!window.history || !history.replaceState) { return; }
|
|
12
|
+
if (href && href.charAt(0) !== '#') { history.replaceState(null, '', href + location.hash); }
|
|
13
|
+
else if (location.hash === '#' + modal.id) { history.replaceState(null, '', location.pathname + location.search); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The dialog showing: drawn open, or the one the address's #id names.
|
|
17
|
+
function shown() {
|
|
18
|
+
var target = location.hash.length > 1 && document.getElementById(location.hash.slice(1));
|
|
19
|
+
var open = document.querySelector('.modal.is-open');
|
|
20
|
+
if (open) { return open; }
|
|
21
|
+
var open = target && /(^|\s)modal(\s|$)/.test(target.className) && !/\bis-closed\b/.test(target.className);
|
|
22
|
+
return open ? target : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The node or its nearest ancestor with the class: old browsers have no closest().
|
|
26
|
+
function up(node, name) {
|
|
27
|
+
var pattern = new RegExp('(^|\\s)' + name + '(\\s|$)');
|
|
28
|
+
while (node && node.nodeType === 1 && !pattern.test(node.className)) { node = node.parentNode; }
|
|
29
|
+
return node && node.nodeType === 1 ? node : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The ×, or the dimmed page around the box (the dialog itself, not what it holds).
|
|
33
|
+
document.addEventListener('click', function (event) {
|
|
34
|
+
var x = up(event.target, 'modal-x');
|
|
35
|
+
var modal = x ? up(x, 'modal') : /(^|\s)modal(\s|$)/.test(event.target.className) && event.target;
|
|
36
|
+
if (!modal) { return; }
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
close(modal);
|
|
39
|
+
});
|
|
40
|
+
document.addEventListener('keydown', function (event) {
|
|
41
|
+
if (event.key !== 'Escape' && event.key !== 'Esc' && event.keyCode !== 27) { return; }
|
|
42
|
+
var modal = shown();
|
|
43
|
+
if (modal) { event.preventDefault(); close(modal); }
|
|
44
|
+
});
|
|
45
|
+
window.addEventListener('hashchange', function () {
|
|
46
|
+
var target = location.hash.length > 1 && document.getElementById(location.hash.slice(1));
|
|
47
|
+
if (target) { target.className = target.className.replace(/\s*\bis-closed\b/g, ''); }
|
|
48
|
+
});
|
|
49
|
+
})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The 6-digit code of render "easy/pin_code": its six slots show what its one real field holds, the next one to fill
|
|
2
|
+
// lit, and six digits submit the form without a click, typed, pasted or offered by the phone from the email. A field
|
|
3
|
+
// the page empties redraws on an input event; a code refused is the page's to mark, is-invalid on the .pin-code,
|
|
4
|
+
// which the next digit clears. A .pin-code already drawn is left alone, so including the script twice is harmless.
|
|
5
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
6
|
+
(function () {
|
|
7
|
+
if (!document.querySelectorAll || !document.addEventListener) { return; }
|
|
8
|
+
|
|
9
|
+
// The form's own submit, so its handlers run first: requestSubmit, or an old browser's submit event.
|
|
10
|
+
function submit(form) {
|
|
11
|
+
if (form.requestSubmit) { form.requestSubmit(); return; }
|
|
12
|
+
var event = document.createEvent('Event');
|
|
13
|
+
event.initEvent('submit', true, true);
|
|
14
|
+
if (form.dispatchEvent(event)) { form.submit(); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function drawn(pin) {
|
|
18
|
+
var input = pin.querySelector('.pin-code-input'), slots = pin.querySelectorAll('.pin-code-slots span');
|
|
19
|
+
function draw() {
|
|
20
|
+
var value = input.value.replace(/\D/g, '').slice(0, 6), focused = document.activeElement === input;
|
|
21
|
+
if (value !== input.value) { input.value = value; }
|
|
22
|
+
for (var i = 0; i < slots.length; i++) {
|
|
23
|
+
slots[i].textContent = value.charAt(i);
|
|
24
|
+
slots[i].className = focused && i === Math.min(value.length, 5) ? 'is-active' : '';
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
input.addEventListener('input', function () {
|
|
29
|
+
pin.className = pin.className.replace(/\s*\bis-invalid\b/g, '');
|
|
30
|
+
input.removeAttribute('aria-invalid');
|
|
31
|
+
if (draw().length === 6 && input.form) { submit(input.form); }
|
|
32
|
+
});
|
|
33
|
+
input.addEventListener('focus', draw);
|
|
34
|
+
input.addEventListener('blur', draw);
|
|
35
|
+
pin.className += ' is-drawn';
|
|
36
|
+
draw();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var pins = document.querySelectorAll('.pin-code');
|
|
40
|
+
for (var i = 0; i < pins.length; i++) {
|
|
41
|
+
if (!/\bis-drawn\b/.test(pins[i].className)) { drawn(pins[i]); }
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The sort button of easy/report_head: flips every list data-sort-list names, each by its rows' data-sort, remembered
|
|
2
|
+
// in this browser. ES5 alone. A page that redraws its list calls window.easyReportSort() after. Without this script the
|
|
3
|
+
// list stays as the server sent it, and the button hidden.
|
|
4
|
+
(function () {
|
|
5
|
+
// A page with two heads links the script twice: the second would flip every list back.
|
|
6
|
+
if (window.easyReportSort) { return; }
|
|
7
|
+
|
|
8
|
+
function saved(head) {
|
|
9
|
+
try {
|
|
10
|
+
return localStorage.getItem('report-sort:' + head.getAttribute('data-sort-list'));
|
|
11
|
+
} catch (blocked) { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function save(head, order) {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem('report-sort:' + head.getAttribute('data-sort-list'), order);
|
|
17
|
+
} catch (blocked) { return; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function order(head) { return head.easyOrder || saved(head) || head.getAttribute('data-sort-default'); }
|
|
21
|
+
|
|
22
|
+
function compared(a, b) {
|
|
23
|
+
return a.getAttribute('data-sort').localeCompare(b.getAttribute('data-sort'), undefined, { numeric: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function apply(head) {
|
|
27
|
+
var lists = document.querySelectorAll(head.getAttribute('data-sort-list'));
|
|
28
|
+
var item = head.querySelector('.sort-button-item');
|
|
29
|
+
if (!lists.length || !item) { return; }
|
|
30
|
+
var way = order(head), newest = way === 'newest';
|
|
31
|
+
Array.prototype.forEach.call(lists, function (list) {
|
|
32
|
+
Array.prototype.slice.call(list.children).filter(function (row) { return row.hasAttribute('data-sort'); })
|
|
33
|
+
.sort(function (a, b) { return newest ? compared(b, a) : compared(a, b); })
|
|
34
|
+
.forEach(function (row) { list.appendChild(row); });
|
|
35
|
+
});
|
|
36
|
+
item.hidden = false;
|
|
37
|
+
item.querySelector('.js-sort-label').textContent = newest ? 'Newest first' : 'Oldest first';
|
|
38
|
+
var icon = item.querySelector('use'), ref = icon.getAttribute('href').split('#')[0] + '#i-' + way;
|
|
39
|
+
icon.setAttribute('href', ref);
|
|
40
|
+
icon.setAttribute('xlink:href', ref);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.easyReportSort = function () {
|
|
44
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-sort-list]'), apply);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
document.addEventListener('click', function (event) {
|
|
48
|
+
var button = event.target.closest && event.target.closest('.js-sort');
|
|
49
|
+
var head = button && button.closest('[data-sort-list]');
|
|
50
|
+
if (!head) { return; }
|
|
51
|
+
head.easyOrder = order(head) === 'newest' ? 'oldest' : 'newest';
|
|
52
|
+
save(head, head.easyOrder);
|
|
53
|
+
apply(head);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
window.easyReportSort();
|
|
57
|
+
}());
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The search box's placeholder types what the box finds, one example after another, until the visitor types their
|
|
2
|
+
// own: each app gives its examples on the field, <input data-rotating-placeholder='["N714AD", "O-320-H2AD"]'>, and
|
|
3
|
+
// its resting placeholder, which comes back once something is typed. Where motion is turned off, it holds still.
|
|
4
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
5
|
+
(function () {
|
|
6
|
+
if (!document.querySelectorAll || !window.JSON) { return; }
|
|
7
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return; }
|
|
8
|
+
|
|
9
|
+
function rotate(input) {
|
|
10
|
+
var examples;
|
|
11
|
+
try { examples = JSON.parse(input.getAttribute('data-rotating-placeholder')); } catch (err) { return; }
|
|
12
|
+
if (!examples || !examples.length) { return; }
|
|
13
|
+
var resting = input.getAttribute('placeholder') || examples[0], ex = 0, at = 0, typing = null;
|
|
14
|
+
function tick() {
|
|
15
|
+
var word = examples[ex];
|
|
16
|
+
if (at <= word.length) {
|
|
17
|
+
input.setAttribute('placeholder', word.slice(0, at) || word.charAt(0));
|
|
18
|
+
at++;
|
|
19
|
+
typing = setTimeout(tick, at > word.length ? 1600 : 140);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ex = (ex + 1) % examples.length; at = 1; typing = setTimeout(tick, 140);
|
|
23
|
+
}
|
|
24
|
+
// Typing stops the show; being in the field doesn't, nor a key that typed nothing (the Tab that got there).
|
|
25
|
+
function typed() {
|
|
26
|
+
if (input.value && typing) { clearTimeout(typing); typing = null; input.setAttribute('placeholder', resting); }
|
|
27
|
+
}
|
|
28
|
+
input.addEventListener('input', typed);
|
|
29
|
+
input.addEventListener('keyup', typed);
|
|
30
|
+
typing = setTimeout(tick, 900);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var inputs = document.querySelectorAll('[data-rotating-placeholder]');
|
|
34
|
+
for (var i = 0; i < inputs.length; i++) { rotate(inputs[i]); }
|
|
35
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A step's foot (step-foot) is in the page's flow, at the step's end. Once the step's top has left the window (the
|
|
2
|
+
// steps bar, or what data-top names) the foot also keeps to the window's foot (is-stuck), coming up from below
|
|
3
|
+
// (is-rising, until it has risen) unless its own place is already in view, as at the end of a short step; back at the
|
|
4
|
+
// top it lets go.
|
|
5
|
+
// Without this script the foot stays at the step's end. ES5 alone, for the browsers that can watch an element.
|
|
6
|
+
(function () {
|
|
7
|
+
var foot = document.querySelector && document.querySelector('.step-foot');
|
|
8
|
+
var top = foot && document.querySelector(foot.getAttribute('data-top') || '.steps-bar');
|
|
9
|
+
if (!top || !window.IntersectionObserver) { return; }
|
|
10
|
+
new IntersectionObserver(function (entries) {
|
|
11
|
+
var gone = !entries[0].isIntersecting && entries[0].boundingClientRect.top < 0;
|
|
12
|
+
var below = foot.getBoundingClientRect().top >= window.innerHeight;
|
|
13
|
+
foot.className = foot.className.replace(/ ?is-(stuck|rising)/g, '') + (gone ? ' is-stuck' + (below ? ' is-rising' : '') : '');
|
|
14
|
+
}).observe(top);
|
|
15
|
+
// Risen, it draws nothing below itself any longer: at the page's end the site's footer is there.
|
|
16
|
+
foot.addEventListener('animationend', function () { foot.className = foot.className.replace(/ ?is-rising/g, ''); });
|
|
17
|
+
})();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Switches over a list: inside js-switches, each count of a switches row (li[data-state]) shows or hides the rows of
|
|
2
|
+
// that state, anything else carrying the same data-state; one with data-off="true" starts off. Words typed in
|
|
3
|
+
// js-words keep the rows that have them all outside their forms, and are marked in them (easy/highlight). A js-none line shows while
|
|
4
|
+
// nothing beside it is left to show, a js-group hides while nothing inside it is, and a tab (js-tabs) whose number is
|
|
5
|
+
// a js-count counts the rows left in its panel that carry data-counted. A row carrying data-keep shows whatever its
|
|
6
|
+
// switch says, until a switch is flipped or a word typed. A page that redraws its rows or its switches calls
|
|
7
|
+
// window.easySwitches() after. Without the script every row shows, the counts are plain and the search hidden.
|
|
8
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
9
|
+
(function () {
|
|
10
|
+
// A page with two heads links the script twice: its boxes are wired once.
|
|
11
|
+
if (window.easySwitches || !document.querySelectorAll || !document.addEventListener) { return; }
|
|
12
|
+
var drawn = [];
|
|
13
|
+
|
|
14
|
+
function each(list, callback) { for (var i = 0; i < list.length; i++) { callback(list[i]); } }
|
|
15
|
+
function has(el, name) { return (' ' + el.className + ' ').indexOf(' ' + name + ' ') >= 0; }
|
|
16
|
+
function within(el, name, stop) {
|
|
17
|
+
for (; el && el !== stop; el = el.parentNode) { if (el.className && has(el, name)) { return el; } }
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// What a row says, without what its forms ask: "Complied with" in every card's form is no reason to show them all.
|
|
21
|
+
function said(node) {
|
|
22
|
+
var text = '';
|
|
23
|
+
for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
24
|
+
if (child.nodeType === 3) { text += child.nodeValue; } else if (child.nodeType === 1 && child.nodeName !== 'FORM') { text += said(child); }
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
function holds(el, rows) {
|
|
29
|
+
for (var i = 0; i < rows.length; i++) { if (el.contains(rows[i])) { return true; } }
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function wire(box) {
|
|
34
|
+
var on = box.easySwitches = {}, field = box.querySelector('.js-words'), input = field && field.querySelector('input');
|
|
35
|
+
function switches() { return box.querySelectorAll('.switches li[data-state]'); }
|
|
36
|
+
function toggle(target) {
|
|
37
|
+
for (; target && target !== box; target = target.parentNode) {
|
|
38
|
+
if (target.getAttribute && target.getAttribute('data-state') && within(target, 'switches', box)) { return target; }
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shown(row, want) {
|
|
44
|
+
if (on[row.getAttribute('data-state')] === false && !row.hasAttribute('data-keep')) { return false; }
|
|
45
|
+
var text = want.join('') ? said(row).toLowerCase() : '';
|
|
46
|
+
for (var w = 0; w < want.length; w++) { if (text.indexOf(want[w]) < 0) { return false; } }
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function draw() {
|
|
51
|
+
var want = (input ? input.value : '').toLowerCase().split(/\s+/), left = [];
|
|
52
|
+
each(switches(), function (li) {
|
|
53
|
+
var state = li.getAttribute('data-state');
|
|
54
|
+
if (on[state] === undefined) { on[state] = li.getAttribute('data-off') !== 'true'; }
|
|
55
|
+
li.className = li.className.replace(/ ?(switches-toggle|is-off)\b/g, '') + ' switches-toggle' + (on[state] ? '' : ' is-off');
|
|
56
|
+
li.setAttribute('role', 'button');
|
|
57
|
+
li.setAttribute('tabindex', '0');
|
|
58
|
+
li.setAttribute('aria-pressed', on[state] ? 'true' : 'false');
|
|
59
|
+
});
|
|
60
|
+
each(box.querySelectorAll('[data-state]'), function (row) {
|
|
61
|
+
if (within(row, 'switches', box)) { return; }
|
|
62
|
+
row.hidden = !shown(row, want);
|
|
63
|
+
if (!row.hidden) { left.push(row); }
|
|
64
|
+
});
|
|
65
|
+
each(box.querySelectorAll('.js-none'), function (none) { none.hidden = holds(none.parentNode, left); });
|
|
66
|
+
each(box.querySelectorAll('.js-group'), function (group) { group.hidden = !holds(group, left); });
|
|
67
|
+
each(box.querySelectorAll('.js-tabs a'), function (tab) {
|
|
68
|
+
var count = tab.querySelector('.js-count'), panel = document.getElementById(tab.getAttribute('href').slice(1)), n = 0;
|
|
69
|
+
if (!count || !panel) { return; }
|
|
70
|
+
each(left, function (row) { if (row.hasAttribute('data-counted') && panel.contains(row)) { n++; } });
|
|
71
|
+
count.textContent = n;
|
|
72
|
+
});
|
|
73
|
+
if (window.easyHighlight) { window.easyHighlight(box, left, want); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function changed() {
|
|
77
|
+
each(box.querySelectorAll('[data-keep]'), function (row) { row.removeAttribute('data-keep'); });
|
|
78
|
+
draw();
|
|
79
|
+
}
|
|
80
|
+
function flip(li) { on[li.getAttribute('data-state')] = !on[li.getAttribute('data-state')]; changed(); }
|
|
81
|
+
|
|
82
|
+
box.addEventListener('click', function (event) { var li = toggle(event.target); if (li) { flip(li); } });
|
|
83
|
+
box.addEventListener('keydown', function (event) {
|
|
84
|
+
var li = toggle(event.target);
|
|
85
|
+
if (li && (event.key === 'Enter' || event.key === ' ' || event.keyCode === 13 || event.keyCode === 32)) {
|
|
86
|
+
event.preventDefault(); flip(li);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (input) { field.hidden = false; input.addEventListener('input', changed); }
|
|
90
|
+
drawn.push(draw);
|
|
91
|
+
draw();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.easySwitches = function () { each(drawn, function (draw) { draw(); }); };
|
|
95
|
+
each(document.querySelectorAll('.js-switches'), wire);
|
|
96
|
+
})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Keeps what a js-switches box carrying data-address is set to in the page's address, so the page can be reloaded or
|
|
2
|
+
// shared as it is: ?q= the words typed, ?off= the switches that are off, when they are not the ones that start off
|
|
3
|
+
// ("off=" alone, then, for all of them on). Linked after easy/switches. ES5 alone.
|
|
4
|
+
(function () {
|
|
5
|
+
var box = document.querySelector('.js-switches[data-address]');
|
|
6
|
+
if (!box || !box.easySwitches || !window.history || !history.replaceState) { return; }
|
|
7
|
+
var on = box.easySwitches, input = box.querySelector('.js-words input'), starts = [];
|
|
8
|
+
|
|
9
|
+
function params() {
|
|
10
|
+
var all = {}, pairs = location.search.replace(/^\?/, '').split('&');
|
|
11
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
12
|
+
var pair = pairs[i].split('=');
|
|
13
|
+
if (pairs[i]) { all[decodeURIComponent(pair[0])] = decodeURIComponent((pair[1] || '').replace(/\+/g, ' ')); }
|
|
14
|
+
}
|
|
15
|
+
return all;
|
|
16
|
+
}
|
|
17
|
+
function off() {
|
|
18
|
+
var states = [];
|
|
19
|
+
for (var state in on) { if (on.hasOwnProperty(state) && !on[state]) { states.push(state); } }
|
|
20
|
+
return states.sort().join(',');
|
|
21
|
+
}
|
|
22
|
+
function remember() {
|
|
23
|
+
var all = params(), words = input ? input.value.replace(/^\s+|\s+$/g, '') : '', pairs = [];
|
|
24
|
+
delete all.q; delete all.off;
|
|
25
|
+
if (words) { all.q = words; }
|
|
26
|
+
if (off() !== starts) { all.off = off(); }
|
|
27
|
+
for (var key in all) { if (all.hasOwnProperty(key)) { pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(all[key])); } }
|
|
28
|
+
history.replaceState(history.state, '', location.pathname + (pairs.length ? '?' + pairs.join('&') : '') + location.hash);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
starts = off();
|
|
32
|
+
var asked = params();
|
|
33
|
+
if (asked.off !== undefined) { for (var state in on) { if (on.hasOwnProperty(state)) { on[state] = (',' + asked.off + ',').indexOf(',' + state + ',') < 0; } } }
|
|
34
|
+
if (asked.q && input) { input.value = asked.q; }
|
|
35
|
+
window.easySwitches();
|
|
36
|
+
box.addEventListener('click', remember);
|
|
37
|
+
box.addEventListener('keydown', remember);
|
|
38
|
+
box.addEventListener('input', remember);
|
|
39
|
+
})();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tabs over panels (part-tabs with js-tabs): each tab is a link to its panel's #id. A click shows that panel and hides
|
|
2
|
+
// the others (is-on on the tab, is-off on a panel), and the address takes the #id, which opens the same tab when the
|
|
3
|
+
// page is loaded with it, as does the #id of anything inside a panel. Without the script the panels show one under the other and the tabs jump to each.
|
|
4
|
+
// ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
5
|
+
(function () {
|
|
6
|
+
if (!document.querySelectorAll || !document.addEventListener) { return; }
|
|
7
|
+
|
|
8
|
+
function strip(el, name) { el.className = el.className.replace(new RegExp('\\s*\\b' + name + '\\b', 'g'), ''); }
|
|
9
|
+
function id(link) { return link.getAttribute('href').slice(1); }
|
|
10
|
+
|
|
11
|
+
function wire(links) {
|
|
12
|
+
function show(shown) {
|
|
13
|
+
for (var i = 0; i < links.length; i++) {
|
|
14
|
+
var panel = document.getElementById(id(links[i]));
|
|
15
|
+
strip(links[i], 'is-on'); strip(panel, 'is-off');
|
|
16
|
+
if (id(links[i]) === shown) { links[i].className += ' is-on'; } else { panel.className += ' is-off'; }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (var i = 0; i < links.length; i++) {
|
|
20
|
+
links[i].addEventListener('click', function (event) {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
show(id(this));
|
|
23
|
+
if (window.history && history.replaceState) { history.replaceState(null, '', '#' + id(this)); }
|
|
24
|
+
});
|
|
25
|
+
// The tab the address names, or the one whose panel holds what it names, brought into view: a hidden panel is
|
|
26
|
+
// nowhere to scroll to. Any other address leaves the tab the page was drawn with.
|
|
27
|
+
var named = window.location.hash.slice(1), at = named && document.getElementById(named);
|
|
28
|
+
if (id(links[i]) === named) { show(named); }
|
|
29
|
+
if (at && id(links[i]) !== named && document.getElementById(id(links[i])).contains(at)) { show(id(links[i])); at.scrollIntoView(); }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var lists = document.querySelectorAll('.js-tabs');
|
|
34
|
+
for (var l = 0; l < lists.length; l++) { wire(lists[l].getElementsByTagName('a')); }
|
|
35
|
+
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The theme a reader picks on this browser, light or dark, kept in localStorage and set on <html> before the page
|
|
2
|
+
// paints (layouts/easy/site). Two ways to pick: the header's button (js-theme), which turns to the other one, and
|
|
3
|
+
// radio buttons (inside js-theme-choice, valued light, dark or device), where device forgets the choice and follows
|
|
4
|
+
// the system again. ES5 alone: every easy app has to work on old iPhones, Androids and Windows browsers.
|
|
5
|
+
(function () {
|
|
6
|
+
if (!document.querySelectorAll || !document.addEventListener) { return; }
|
|
7
|
+
var root = document.documentElement;
|
|
8
|
+
|
|
9
|
+
function stored() { var theme = root.getAttribute('data-theme'); return theme === 'dark' || theme === 'light' ? theme : null; }
|
|
10
|
+
function system() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
11
|
+
function each(selector, callback) {
|
|
12
|
+
var list = document.querySelectorAll(selector);
|
|
13
|
+
for (var i = 0; i < list.length; i++) { callback(list[i]); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function paint() {
|
|
17
|
+
var dark = (stored() || system()) === 'dark';
|
|
18
|
+
each('.js-theme', function (button) {
|
|
19
|
+
var label = button.querySelector('.js-theme-label, .theme-toggle-label');
|
|
20
|
+
button.setAttribute('aria-pressed', dark ? 'true' : 'false');
|
|
21
|
+
if (label) { label.textContent = dark ? 'Light theme' : 'Dark theme'; }
|
|
22
|
+
});
|
|
23
|
+
each('.js-theme-choice input', function (radio) { radio.checked = radio.value === (stored() || 'device'); });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pick(theme) {
|
|
27
|
+
if (theme === 'device') { root.removeAttribute('data-theme'); } else { root.setAttribute('data-theme', theme); }
|
|
28
|
+
try {
|
|
29
|
+
if (theme === 'device') { localStorage.removeItem('theme'); } else { localStorage.setItem('theme', theme); }
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
paint();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
each('.js-theme', function (button) {
|
|
35
|
+
button.addEventListener('click', function () { pick((stored() || system()) === 'dark' ? 'light' : 'dark'); });
|
|
36
|
+
});
|
|
37
|
+
each('.js-theme-choice input', function (radio) {
|
|
38
|
+
radio.addEventListener('change', function () { pick(radio.value); });
|
|
39
|
+
});
|
|
40
|
+
paint();
|
|
41
|
+
})();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* The easy design system's base: Outfit (the headings' face, served from here, one variable file
|
|
2
|
+
for every weight as Google Fonts serves it, ranges and all), the reset, type and utilities. */
|
|
3
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:500;font-display:swap;src:url("outfit-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
4
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:500;font-display:swap;src:url("outfit-latin.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
5
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:600;font-display:swap;src:url("outfit-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
6
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:600;font-display:swap;src:url("outfit-latin.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
7
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:700;font-display:swap;src:url("outfit-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
|
8
|
+
@font-face{font-family:"Outfit";font-style:normal;font-weight:700;font-display:swap;src:url("outfit-latin.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
9
|
+
|
|
10
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
11
|
+
a,button{-webkit-tap-highlight-color:rgba(0,0,0,0)}
|
|
12
|
+
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
|
|
13
|
+
html,body{overflow-x:hidden}
|
|
14
|
+
/* hidden makes the body its own scroller, where nothing can stick: clip keeps the page from scrolling sideways without that */
|
|
15
|
+
@supports (overflow-x:clip){html,body{overflow-x:clip}}
|
|
16
|
+
/* The page fills the window at least, its footer last (layouts/easy/site): on a short page the footer sits at the
|
|
17
|
+
window's bottom, never in its middle, as the page grows (easy/layout.css, .page). Old WebKits read the boxes'
|
|
18
|
+
first syntax; a browser without flex boxes draws the footer right after the page, as on any page. Behind it all,
|
|
19
|
+
blue fading to white over its first 250px, however long the page. */
|
|
20
|
+
body{margin:0;background:var(--color-bg);background:var(--color-bg) linear-gradient(180deg,var(--color-sky-band-top),var(--color-sky-band-bottom)) no-repeat 0 0/100% 250px;color:var(--color-text);min-height:100vh;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}
|
|
21
|
+
img,svg{max-width:100%}
|
|
22
|
+
.i{width:1em;height:1em;vertical-align:-.125em;fill:currentColor;flex-shrink:0}
|
|
23
|
+
/* A link goes somewhere: blue words, no box. Underlined inside a sentence; on its own (a title, a list, a menu) the
|
|
24
|
+
underline comes under the pointer, where the blue deepens. :where keeps the hover as light as a class, so any
|
|
25
|
+
component's own rule for its links still wins. */
|
|
26
|
+
a{color:var(--color-primary);text-decoration:none;text-underline-offset:3px;text-decoration-thickness:1px;transition:color .12s}
|
|
27
|
+
p a,dd a,.in-text a{text-decoration:underline}
|
|
28
|
+
:where(a):hover{color:var(--color-primary-strong);text-decoration:underline;text-decoration-thickness:2px}
|
|
29
|
+
a:focus:not(:focus-visible),button:focus:not(:focus-visible),input:focus:not(:focus-visible),summary:focus:not(:focus-visible){outline:0}
|
|
30
|
+
/* The focus ring is the keyboard's (and a text field's, which is always "visible"); a click draws none */
|
|
31
|
+
a:focus-visible,button:focus-visible,input:focus-visible,summary:focus-visible{outline:3px solid var(--color-focus);outline-offset:2px}
|
|
32
|
+
@media (prefers-reduced-motion: reduce){
|
|
33
|
+
*,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}
|
|
34
|
+
}
|
|
35
|
+
.no-wrap{white-space:nowrap}
|
|
36
|
+
.visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
37
|
+
[hidden]{display:none!important}
|
|
38
|
+
|
|
39
|
+
body{font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
40
|
+
h1,h2,h3,.site-brand{font-family:"Outfit","Helvetica Neue",Arial,sans-serif;font-weight:600;line-height:1.2}
|
|
41
|
+
.phone-only{display:none}
|
|
42
|
+
@media (max-width:560px){.phone-hidden{display:none}.phone-only{display:inline}}
|