@knadh/oat 0.3.0 → 0.4.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.
package/css/spinner.css CHANGED
@@ -1,22 +1,47 @@
1
1
  @layer components {
2
- .spinner {
3
- width: 1.5rem;
4
- height: 1.5rem;
5
- border: 2px solid var(--muted);
6
- border-top-color: var(--primary);
7
- border-radius: var(--radius-full);
8
- animation: spin 1s linear infinite;
2
+ [aria-busy="true"] {
3
+ &::before {
4
+ content: "";
5
+ display: inline-block;
6
+ inset: 0;
7
+ margin: auto;
8
+ width: 1.5rem;
9
+ height: 1.5rem;
10
+ border: 2px solid var(--muted);
11
+ border-top-color: var(--primary);
12
+ border-radius: var(--radius-full);
13
+ animation: spin 1s linear infinite;
14
+ text-align: center;
15
+ }
9
16
 
10
- &.small {
17
+ &[data-spinner~="small"]::before {
11
18
  width: 1rem;
12
19
  height: 1rem;
13
20
  }
14
21
 
15
- &.large {
22
+ &[data-spinner~="large"]::before {
16
23
  width: 2rem;
17
24
  height: 2rem;
18
25
  border-width: 3px;
19
26
  }
27
+
28
+ &[data-spinner~="overlay"] {
29
+ position: relative;
30
+
31
+ > * {
32
+ opacity: 0.3;
33
+
34
+ /* "disable" all elements in the container while it's busy */
35
+ pointer-events: none;
36
+ }
37
+
38
+ &::before {
39
+ position: absolute;
40
+ inset: 0;
41
+ margin: auto;
42
+ z-index: 1;
43
+ }
44
+ }
20
45
  }
21
46
 
22
47
  @keyframes spin {
package/css/tabs.css CHANGED
@@ -17,19 +17,18 @@
17
17
  font-weight: var(--font-medium);
18
18
  white-space: nowrap;
19
19
  background-color: transparent;
20
- color: var(--muted-foreground);
20
+ color: var(--foreground);
21
21
  border: none;
22
22
  border-radius: calc(var(--radius-medium) - 2px);
23
23
  cursor: pointer;
24
24
  transition: background-color var(--transition-fast), color var(--transition-fast);
25
25
 
26
26
  &:hover {
27
- color: var(--foreground);
27
+ color: var(--muted-foreground);
28
28
  }
29
29
 
30
30
  &[aria-selected="true"] {
31
31
  background-color: var(--background);
32
- color: var(--foreground);
33
32
  box-shadow: var(--shadow-small);
34
33
  }
35
34
  }
package/css/toast.css CHANGED
@@ -46,6 +46,9 @@
46
46
  }
47
47
 
48
48
  .toast {
49
+ --transition: 300ms;
50
+ --transition-in: calc(var(--transition) - 50ms);
51
+
49
52
  padding: var(--space-5) var(--space-4);
50
53
  max-width: 28rem;
51
54
  min-width: 20rem;
@@ -56,7 +59,7 @@
56
59
  border-inline-start-style: solid;
57
60
  border-radius: var(--radius-medium);
58
61
  box-shadow: var(--shadow-small);
59
- transition: opacity 150ms, transform 150ms, margin 150ms;
62
+ transition: opacity var(--transition-in), transform var(--transition-in), margin var(--transition-in);
60
63
  line-height: 1;
61
64
 
62
65
  .toast-title {
@@ -69,18 +72,23 @@
69
72
 
70
73
  &[data-variant="success"] {
71
74
  border-inline-start-color: var(--success);
75
+ .toast-title {
76
+ color: var(--success);
77
+ }
72
78
  }
73
79
 
74
80
  &[data-variant="danger"] {
75
81
  border-inline-start-color: var(--danger);
82
+ .toast-title {
83
+ color: var(--danger);
84
+ }
76
85
  }
77
86
 
78
87
  &[data-variant="warning"] {
79
88
  border-inline-start-color: var(--warning);
80
- }
81
-
82
- &[data-variant="info"] {
83
- border-inline-start-color: var(--secondary);
89
+ .toast-title {
90
+ color: var(--warning);
91
+ }
84
92
  }
85
93
 
86
94
  & > [data-close] {
@@ -109,7 +117,7 @@
109
117
  padding-block: 0;
110
118
  max-height: 0;
111
119
  overflow: hidden;
112
- transition: opacity 200ms, margin 200ms, padding 200ms, max-height 200ms;
120
+ transition: opacity var(--transition), margin var(--transition), padding var(--transition), max-height var(--transition);
113
121
  }
114
122
  }
115
123
  }
package/css/utilities.css CHANGED
@@ -1,25 +1,9 @@
1
1
  @layer utilities {
2
- [hidden] {
3
- display: none !important;
4
- }
5
-
6
- .sr-only {
7
- position: absolute;
8
- width: 1px;
9
- height: 1px;
10
- padding: 0;
11
- margin: -1px;
12
- overflow: hidden;
13
- clip: rect(0, 0, 0, 0);
14
- white-space: nowrap;
15
- border-width: 0;
16
- }
17
-
18
2
  .text-left { text-align: start; }
19
3
  .text-center { text-align: center; }
20
4
  .text-right { text-align: end; }
21
5
  .text-light { color: var(--muted-foreground); }
22
- .text-lighter { color: var(--muted-foreground); }
6
+ .text-lighter { color: var(--faint-foreground); }
23
7
 
24
8
  .flex { display: flex; }
25
9
  .flex-col { flex-direction: column; }
@@ -32,6 +16,9 @@
32
16
  display: flex;
33
17
  align-items: center;
34
18
  gap: var(--space-3);
19
+ flex-wrap: wrap;
20
+ align-content: flex-start;
21
+ height: auto;
35
22
  }
36
23
  .vstack {
37
24
  display: flex;
@@ -53,12 +40,12 @@
53
40
  .p-4 { padding: var(--space-4); }
54
41
 
55
42
  .w-100 { width: 100%; }
56
- }
57
43
 
58
- ul, ol {
59
- &.unstyled {
60
- list-style: none;
61
- padding-inline-start: 0;
62
- margin-inline-start: 0;
44
+ ul, ol {
45
+ &.unstyled {
46
+ list-style: none;
47
+ padding-inline-start: 0;
48
+ margin-inline-start: 0;
49
+ }
63
50
  }
64
51
  }
package/js/base.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // oat - Base Web Component Class
2
2
  // Provides lifecycle management, event handling, and utilities.
3
3
 
4
- class OtBase extends HTMLElement {
4
+ export class OtBase extends HTMLElement {
5
5
  #initialized = false;
6
6
 
7
7
  // Called when element is added to DOM.
@@ -23,9 +23,6 @@ class OtBase extends HTMLElement {
23
23
  this.init();
24
24
  }
25
25
 
26
- // Override in WebComponent subclasses for init logic.
27
- init() {}
28
-
29
26
  // Called when element is removed from DOM.
30
27
  disconnectedCallback() {
31
28
  this.cleanup();
@@ -41,6 +38,29 @@ class OtBase extends HTMLElement {
41
38
  if (handler) handler.call(this, event);
42
39
  }
43
40
 
41
+ // Given a keyboard event (left, right, home, end), the current selection idx
42
+ // total items in a list, return 0-n index of the next/previous item
43
+ // for doing a roving keyboard nav.
44
+ keyNav(event, idx, len, prevKey, nextKey, homeEnd = false) {
45
+ const { key } = event;
46
+ let next = -1;
47
+
48
+ if (key === nextKey) {
49
+ next = (idx + 1) % len;
50
+ } else if (key === prevKey) {
51
+ next = (idx - 1 + len) % len;
52
+ } else if (homeEnd) {
53
+ if (key === 'Home') {
54
+ next = 0;
55
+ } else if (key === 'End') {
56
+ next = len - 1;
57
+ }
58
+ }
59
+
60
+ if (next >= 0) event.preventDefault();
61
+ return next;
62
+ }
63
+
44
64
  // Emit a custom event.
45
65
  emit(name, detail = null) {
46
66
  return this.dispatchEvent(new CustomEvent(name, {
@@ -51,20 +71,6 @@ class OtBase extends HTMLElement {
51
71
  }));
52
72
  }
53
73
 
54
- // Get boolean attribute value.
55
- getBool(name) {
56
- return this.hasAttribute(name);
57
- }
58
-
59
- // Set or remove boolean attribute.
60
- setBool(name, value) {
61
- if (value) {
62
- this.setAttribute(name, '');
63
- } else {
64
- this.removeAttribute(name);
65
- }
66
- }
67
-
68
74
  // Query selector within this element.
69
75
  $(selector) {
70
76
  return this.querySelector(selector);
@@ -81,15 +87,10 @@ class OtBase extends HTMLElement {
81
87
  }
82
88
  }
83
89
 
84
- // Export for use in other files
85
- if (typeof window !== 'undefined') {
86
- window.OtBase = OtBase;
87
- }
88
-
89
90
  // Polyfill for command/commandfor (Safari)
90
91
  if (!('commandForElement' in HTMLButtonElement.prototype)) {
91
92
  document.addEventListener('click', e => {
92
- const btn = e.target.closest('[commandfor]');
93
+ const btn = e.target.closest('button[commandfor]');
93
94
  if (!btn) return;
94
95
 
95
96
  const target = document.getElementById(btn.getAttribute('commandfor'));
package/js/dropdown.js CHANGED
@@ -12,10 +12,13 @@
12
12
  * </ot-dropdown>
13
13
  */
14
14
 
15
+ import { OtBase } from './base.js';
16
+
15
17
  class OtDropdown extends OtBase {
16
18
  #menu;
17
19
  #trigger;
18
20
  #position;
21
+ #items;
19
22
 
20
23
  init() {
21
24
  this.#menu = this.$('[popover]');
@@ -29,9 +32,12 @@ class OtDropdown extends OtBase {
29
32
  this.#position = () => {
30
33
  // Position has to be calculated and applied manually because
31
34
  // popover positioning is like fixed, relative to the window.
32
- const rect = this.#trigger.getBoundingClientRect();
33
- this.#menu.style.top = `${rect.bottom}px`;
34
- this.#menu.style.left = `${rect.left}px`;
35
+ const r = this.#trigger.getBoundingClientRect();
36
+ const m = this.#menu.getBoundingClientRect();
37
+
38
+ // Flip if menu overflows viewport.
39
+ this.#menu.style.top = `${r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom}px`;
40
+ this.#menu.style.left = `${r.left + m.width > window.innerWidth ? r.right - m.width : r.left}px`;
35
41
  };
36
42
  }
37
43
 
@@ -39,10 +45,13 @@ class OtDropdown extends OtBase {
39
45
  if (e.newState === 'open') {
40
46
  this.#position();
41
47
  window.addEventListener('scroll', this.#position, true);
42
- this.$('[role="menuitem"]')?.focus();
48
+ window.addEventListener('resize', this.#position);
49
+ this.#items = this.$$('[role="menuitem"]');
50
+ this.#items[0]?.focus();
43
51
  this.#trigger.ariaExpanded = 'true';
44
52
  } else {
45
- window.removeEventListener('scroll', this.#position, true);
53
+ this.cleanup();
54
+ this.#items = null;
46
55
  this.#trigger.ariaExpanded = 'false';
47
56
  this.#trigger.focus();
48
57
  }
@@ -51,23 +60,14 @@ class OtDropdown extends OtBase {
51
60
  onkeydown(e) {
52
61
  if (!e.target.matches('[role="menuitem"]')) return;
53
62
 
54
- const items = this.$$('[role="menuitem"]');
55
- const idx = items.indexOf(e.target);
56
-
57
- switch (e.key) {
58
- case 'ArrowDown':
59
- e.preventDefault();
60
- items[(idx + 1) % items.length]?.focus();
61
- break;
62
- case 'ArrowUp':
63
- e.preventDefault();
64
- items[idx - 1 < 0 ? items.length - 1 : idx - 1]?.focus();
65
- break;
66
- }
63
+ const idx = this.#items.indexOf(e.target);
64
+ const next = this.keyNav(e, idx, this.#items.length, 'ArrowUp', 'ArrowDown', true);
65
+ if (next >= 0) this.#items[next].focus();
67
66
  }
68
67
 
69
68
  cleanup() {
70
69
  window.removeEventListener('scroll', this.#position, true);
70
+ window.removeEventListener('resize', this.#position);
71
71
  }
72
72
  }
73
73
 
package/js/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import './base.js';
2
+ import './tabs.js';
3
+ import './dropdown.js';
4
+ import './tooltip.js';
5
+ import './sidebar.js';
6
+ import { toast, toastEl, toastClear } from './toast.js';
7
+
8
+ // Register the global window.ot.* APIs.
9
+ const ot = window.ot || (window.ot = {});
10
+ ot.toast = toast;
11
+ ot.toast.el = toastEl;
12
+ ot.toast.clear = toastClear;
package/js/sidebar.js CHANGED
@@ -7,5 +7,16 @@ document.addEventListener('click', (e) => {
7
7
  if (toggle) {
8
8
  const layout = toggle.closest('[data-sidebar-layout]');
9
9
  layout?.toggleAttribute('data-sidebar-open');
10
+ return;
11
+ }
12
+
13
+ // Dismiss sidebar when clicking outside (when sidebar is not an overlay).
14
+ if (!e.target.closest('[data-sidebar]')) {
15
+ const layout = document.querySelector('[data-sidebar-layout][data-sidebar-open]');
16
+ // Hardcode breakpoint (for now) as there's no way to use a CSS variable in
17
+ // the @media{} query which could've been picked up here.
18
+ if (layout && window.matchMedia('(max-width: 768px)').matches) {
19
+ layout.removeAttribute('data-sidebar-open');
20
+ }
10
21
  }
11
22
  });
package/js/tabs.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * </ot-tabs>
14
14
  */
15
15
 
16
+ import { OtBase } from './base.js';
17
+
16
18
  class OtTabs extends OtBase {
17
19
  #tabs = [];
18
20
  #panels = [];
@@ -39,11 +41,11 @@ class OtTabs extends OtBase {
39
41
  panel.id = panelId;
40
42
  tab.setAttribute('aria-controls', panelId);
41
43
  panel.setAttribute('aria-labelledby', tabId);
42
-
43
- tab.addEventListener('click', this);
44
- tab.addEventListener('keydown', this);
45
44
  });
46
45
 
46
+ tablist.addEventListener('click', this);
47
+ tablist.addEventListener('keydown', this);
48
+
47
49
  // Find initially active tab or default to first.
48
50
  const activeTab = this.#tabs.findIndex(t => t.ariaSelected === 'true');
49
51
  this.#activate(activeTab >= 0 ? activeTab : 0);
@@ -55,26 +57,13 @@ class OtTabs extends OtBase {
55
57
  }
56
58
 
57
59
  onkeydown(e) {
58
- const { key } = e;
59
- const idx = this.activeIndex;
60
- let newIdx = idx;
61
-
62
- switch (key) {
63
- case 'ArrowLeft':
64
- e.preventDefault();
65
- newIdx = idx - 1;
66
- if (newIdx < 0) newIdx = this.#tabs.length - 1;
67
- break;
68
- case 'ArrowRight':
69
- e.preventDefault();
70
- newIdx = (idx + 1) % this.#tabs.length;
71
- break;
72
- default:
73
- return;
74
- }
60
+ if (!e.target.closest('[role="tab"]')) return;
75
61
 
76
- this.#activate(newIdx);
77
- this.#tabs[newIdx].focus();
62
+ const next = this.keyNav(e, this.activeIndex, this.#tabs.length, 'ArrowLeft', 'ArrowRight');
63
+ if (next >= 0) {
64
+ this.#activate(next);
65
+ this.#tabs[next].focus();
66
+ }
78
67
  }
79
68
 
80
69
  #activate(idx) {
package/js/toast.js CHANGED
@@ -8,133 +8,137 @@
8
8
  * ot.toast('Something went wrong.', 'Error', { variant: 'danger', placement: 'bottom-center' })
9
9
  *
10
10
  * // Custom markup
11
- * ot.toastEl(element)
12
- * ot.toastEl(element, { duration: 4000, placement: 'bottom-center' })
13
- * ot.toastEl(document.querySelector('#my-template'))
11
+ * ot.toast.el(element)
12
+ * ot.toast.el(element, { duration: 4000, placement: 'bottom-center' })
13
+ * ot.toast.el(document.querySelector('#my-template'))
14
14
  */
15
15
 
16
- const ot = window.ot || (window.ot = {});
16
+ const toasts = {};
17
17
 
18
- const containers = {};
19
- const DEFAULT_DURATION = 4000;
20
- const DEFAULT_PLACEMENT = 'top-right';
21
-
22
- function getContainer(placement) {
23
- if (!containers[placement]) {
18
+ function _get(placement) {
19
+ if (!toasts[placement]) {
24
20
  const el = document.createElement('div');
25
21
  el.className = 'toast-container';
26
22
  el.setAttribute('popover', 'manual');
27
23
  el.setAttribute('data-placement', placement);
28
24
  document.body.appendChild(el);
29
- containers[placement] = el;
25
+ toasts[placement] = el;
30
26
  }
31
27
 
32
- return containers[placement];
28
+ return toasts[placement];
33
29
  }
34
30
 
35
- function show(toast, options = {}) {
36
- const { placement = DEFAULT_PLACEMENT, duration = DEFAULT_DURATION } = options;
37
- const container = getContainer(placement);
31
+ function _show(el, options = {}) {
32
+ const { placement = 'top-right', duration = 4000 } = options;
33
+ const p = _get(placement);
38
34
 
39
- toast.classList.add('toast');
35
+ el.classList.add('toast');
40
36
 
41
37
  let timeout;
42
38
 
43
39
  // Pause on hover.
44
- toast.onmouseenter = () => clearTimeout(timeout);
45
- toast.onmouseleave = () => {
40
+ el.onmouseenter = () => clearTimeout(timeout);
41
+ el.onmouseleave = () => {
46
42
  if (duration > 0) {
47
- timeout = setTimeout(() => removeToast(toast, container), duration);
43
+ timeout = setTimeout(() => _remove(el, p), duration);
48
44
  }
49
45
  };
50
46
 
51
47
  // Show with animation.
52
- toast.setAttribute('data-entering', '');
53
- container.appendChild(toast);
54
- container.showPopover();
48
+ el.setAttribute('data-entering', '');
49
+ p.appendChild(el);
50
+ p.showPopover();
55
51
 
56
52
  // Double RAF to compute styles before transition starts.
57
53
  requestAnimationFrame(() => {
58
54
  requestAnimationFrame(() => {
59
- toast.removeAttribute('data-entering');
55
+ el.removeAttribute('data-entering');
60
56
  });
61
57
  });
62
58
 
63
59
  if (duration > 0) {
64
- timeout = setTimeout(() => removeToast(toast, container), duration);
60
+ timeout = setTimeout(() => _remove(el, p), duration);
61
+ }
62
+
63
+ return el;
64
+ }
65
+
66
+ function _remove(el, container) {
67
+ // Ignore if already in the process of exiting.
68
+ if (el.hasAttribute('data-exiting')) {
69
+ return;
65
70
  }
71
+ el.setAttribute('data-exiting', '');
72
+
73
+ const cleanup = () => {
74
+ el.remove();
75
+ if (!container.children.length) {
76
+ container.hidePopover();
77
+ }
78
+ };
66
79
 
67
- return toast;
80
+ el.addEventListener('transitionend', cleanup, { once: true });
81
+
82
+ // Couldn't confirm what unit this actually returns across browsers, so
83
+ // assume that it could be ms or s. Also, setTimeou() is required because
84
+ // there's no guarantee that the `transitionend` event will always fire,
85
+ // eg: clients that disable animations.
86
+ const t = getComputedStyle(el).getPropertyValue('--transition').trim();
87
+ const val = parseFloat(t);
88
+ const ms = t.endsWith('ms') ? val : val * 1000;
89
+ setTimeout(cleanup, ms);
68
90
  }
69
91
 
70
- // Simple text toast.
71
- ot.toast = function (message, title, options = {}) {
92
+ // Show a text toast.
93
+ export function toast(message, title, options = {}) {
72
94
  const { variant = 'info', ...rest } = options;
73
95
 
74
- const toast = document.createElement('output');
75
- toast.setAttribute('data-variant', variant);
96
+ const el = document.createElement('output');
97
+ el.setAttribute('data-variant', variant);
76
98
 
77
99
  if (title) {
78
100
  const titleEl = document.createElement('h6');
79
101
  titleEl.className = 'toast-title';
80
- titleEl.style.color = `var(--${variant})`;
81
102
  titleEl.textContent = title;
82
- toast.appendChild(titleEl);
103
+ el.appendChild(titleEl);
83
104
  }
84
105
 
85
- if (message) {
86
- const msgEl = document.createElement('div');
87
- msgEl.className = 'toast-message';
88
- msgEl.textContent = message;
89
- toast.appendChild(msgEl);
90
- }
106
+ const msgEl = document.createElement('div');
107
+ msgEl.className = 'toast-message';
108
+ msgEl.textContent = message;
109
+ el.appendChild(msgEl);
91
110
 
92
- return show(toast, rest);
93
- };
111
+ return _show(el, rest);
112
+ }
94
113
 
95
114
  // Element-based toast.
96
- ot.toastEl = function (el, options = {}) {
97
- let toast;
115
+ export function toastEl(el, options = {}) {
116
+ let t;
98
117
 
99
118
  if (el instanceof HTMLTemplateElement) {
100
- toast = el.content.firstElementChild.cloneNode(true);
101
- } else if (typeof el === 'string') {
102
- toast = document.querySelector(el).cloneNode(true);
103
- } else {
104
- toast = el.cloneNode(true);
119
+ t = el.content.firstElementChild?.cloneNode(true);
120
+ } else if (el) {
121
+ t = el.cloneNode(true);
105
122
  }
106
123
 
107
- toast.removeAttribute('id');
108
-
109
- return show(toast, options);
110
- };
111
-
112
- function removeToast(toast, container) {
113
- if (toast.hasAttribute('data-exiting')) {
124
+ if (!t) {
114
125
  return;
115
126
  }
116
- toast.setAttribute('data-exiting', '');
117
127
 
118
- const cleanup = () => {
119
- toast.remove();
120
- if (!container.children.length) {
121
- container.hidePopover();
122
- }
123
- };
128
+ t.removeAttribute('id');
124
129
 
125
- toast.addEventListener('transitionend', cleanup, { once: true });
126
- setTimeout(cleanup, 200);
130
+ return _show(t, options);
127
131
  }
128
132
 
129
133
  // Clear all toasts.
130
- ot.toast.clear = function (placement) {
131
- if (placement && containers[placement]) {
132
- containers[placement].innerHTML = '';
133
- containers[placement].hidePopover();
134
+ export function toastClear(placement) {
135
+ if (placement && toasts[placement]) {
136
+ toasts[placement].innerHTML = '';
137
+ toasts[placement].hidePopover();
134
138
  } else {
135
- Object.values(containers).forEach(c => {
139
+ Object.values(toasts).forEach(c => {
136
140
  c.innerHTML = '';
137
141
  c.hidePopover();
138
142
  });
139
143
  }
140
- };
144
+ }
package/js/tooltip.js CHANGED
@@ -5,14 +5,32 @@
5
5
  */
6
6
 
7
7
  document.addEventListener('DOMContentLoaded', () => {
8
- document.querySelectorAll('[title]').forEach(el => {
9
- const text = el.getAttribute('title');
10
- if (text) {
11
- el.setAttribute('data-tooltip', text);
12
- if (!el.hasAttribute('aria-label')) {
13
- el.setAttribute('aria-label', text);
14
- }
15
- el.removeAttribute('title');
8
+ const _attrib = 'title', _sel = '[title]';
9
+ const apply = el => {
10
+ const t = el.getAttribute(_attrib);
11
+ if (!t) return;
12
+ el.setAttribute('data-tooltip', t);
13
+ el.hasAttribute('aria-label') || el.setAttribute('aria-label', t);
14
+
15
+ // Kill the original 'title'.
16
+ el.removeAttribute(_attrib);
17
+ };
18
+
19
+ // Apply to all elements on load.
20
+ document.querySelectorAll(_sel).forEach(apply);
21
+
22
+ // Apply to new elements.
23
+ new MutationObserver(muts => {
24
+ for (const m of muts) {
25
+ apply(m.target);
26
+
27
+ for (const n of m.addedNodes)
28
+ if (n.nodeType === 1) {
29
+ apply(n);
30
+ n.querySelectorAll(_sel).forEach(apply);
31
+ }
16
32
  }
33
+ }).observe(document.body, {
34
+ childList: true, subtree: true, attributes: true, attributeFilter: [_attrib]
17
35
  });
18
36
  });