@jungherz-de/notionkit-elements 1.11.1 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungherz-de/notionkit-elements",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
5
5
  "license": "MIT",
6
6
  "author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
@@ -62,10 +62,10 @@
62
62
  "dark-mode"
63
63
  ],
64
64
  "peerDependencies": {
65
- "@jungherz-de/notionkit": ">=1.11.0"
65
+ "@jungherz-de/notionkit": ">=1.12.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@jungherz-de/notionkit": "^1.11.1",
68
+ "@jungherz-de/notionkit": "^1.12.0",
69
69
  "@playwright/test": "^1.50.0",
70
70
  "@rollup/plugin-node-resolve": "^15.0.0",
71
71
  "@rollup/plugin-terser": "^0.4.0",
@@ -9,7 +9,10 @@ import { renderPropertyCell, compareBy } from '../../util/property-cell.js';
9
9
  // { row, id, key, cell } – `key` and `cell` say which cell was hit, to open
10
10
  // an editor there, like the date picker under a due date – checkboxes
11
11
  // nk-change, headers nk-action { action: 'sort' } (and sort locally
12
- // with `sortable`), the add row nk-action { action: 'new-row' }.
12
+ // with `sortable`), the add row nk-action { action: 'new-row' }. A column of
13
+ // type 'actions' (NotionKit 1.12.0) sets buttons in each row; a click on one
14
+ // fires nk-action { action, row, id, anchor } instead of selecting the row,
15
+ // and its header sorts nothing.
13
16
  class NkTableView extends NkElement {
14
17
  static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
15
18
 
@@ -39,9 +42,10 @@ class NkTableView extends NkElement {
39
42
  this._table.classList.toggle('wrap', this.getBoolAttr('wrap'));
40
43
  const tr = document.createElement('tr');
41
44
  for (const col of this._columns) {
42
- const th = this.createElement('th', [], { 'data-key': col.key, scope: 'col' });
45
+ const actions = col.type === 'actions';
46
+ const th = this.createElement('th', actions ? ['actions'] : [], actions ? { scope: 'col' } : { 'data-key': col.key, scope: 'col' });
43
47
  if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
44
- th.appendChild(document.createTextNode(col.label ?? col.key));
48
+ th.appendChild(document.createTextNode(col.label ?? (actions ? '' : col.key)));
45
49
  if (col.key === this.getAttribute('sort-key')) th.appendChild(document.createTextNode(this.getAttribute('sort-dir') === 'desc' ? ' ↓' : ' ↑'));
46
50
  if (col.width) th.style.width = col.width;
47
51
  tr.appendChild(th);
@@ -74,6 +78,12 @@ class NkTableView extends NkElement {
74
78
  if (ok && this.getBoolAttr('sortable')) { this.setAttribute('sort-key', key); this.setAttribute('sort-dir', dir); }
75
79
  return;
76
80
  }
81
+ const act = e.target.closest('button[data-action]');
82
+ if (act) {
83
+ const row = this._rowById(act.closest('tr[data-id]')?.dataset.id);
84
+ if (!act.disabled) this.emit('nk-action', { action: act.dataset.action, row, id: row?.id, anchor: act });
85
+ return;
86
+ }
77
87
  if (e.target.closest('input[type=checkbox], a')) return;
78
88
  const tr = e.target.closest('tr[data-id]');
79
89
  if (!tr) return;
@@ -4,9 +4,13 @@ import { NkElement } from '../../base.js';
4
4
  // <nk-panel href="/roadmap" cover="covers/aurora.svg" icon="🚀" title="Roadmap"><p>2 min ago</p></nk-panel>
5
5
  // → <a class="nk-panel" href="/roadmap"><div class="nk-cover"><img src="…" alt=""></div>
6
6
  // <div class="nk-page-icon">🚀</div><h3>Roadmap</h3>…</a>
7
+ // <nk-panel title="Contacts database"><nk-tag slot="end" color="green">Connected</nk-tag>…</nk-panel>
8
+ // → … <div class="p-head"><h3>Contacts database</h3><span class="p-end">…</span></div>
7
9
  // `cover` without a value draws the token gradient, with one a picture; `icon`
8
10
  // overlaps the cover as on a page – a page tile, as on Notion's Home. With
9
11
  // `href` the panel is a link. `title` is the heading, never a tooltip.
12
+ // slot="end" (NotionKit 1.12.0) sits at the right edge of the title – a state
13
+ // as a tag, a button – and moves under it where both do not fit.
10
14
  class NkPanel extends NkElement {
11
15
  static get observedAttributes() { return ['title', 'icon', 'cover', 'href', 'target']; }
12
16
 
@@ -15,6 +19,11 @@ class NkPanel extends NkElement {
15
19
  this._img = this.createElement('img', [], { alt: '' });
16
20
  this._icon = this.createElement('div', ['nk-page-icon']);
17
21
  this._heading = document.createElement('h3');
22
+ this._endSlot = this.createElement('slot', [], { name: 'end' });
23
+ this._end = this.createElement('span', ['p-end']);
24
+ this._end.appendChild(this._endSlot);
25
+ this._head = this.createElement('div', ['p-head']);
26
+ this._head.append(this._heading, this._end);
18
27
  this._slot = document.createElement('slot');
19
28
  this.takeTitle();
20
29
  this._build();
@@ -23,7 +32,7 @@ class NkPanel extends NkElement {
23
32
  _build() {
24
33
  const href = this.getAttribute('href');
25
34
  const box = this.createElement(href ? 'a' : 'div', ['nk-panel']);
26
- box.append(this._cover, this._icon, this._heading, this._slot);
35
+ box.append(this._cover, this._icon, this._head, this._slot);
27
36
  this._wrapper.replaceChildren(box);
28
37
  this._box = box;
29
38
  this._sync();
@@ -39,10 +48,23 @@ class NkPanel extends NkElement {
39
48
  const icon = this.getAttribute('icon');
40
49
  this._icon.textContent = icon || '';
41
50
  this._icon.style.display = icon ? '' : 'none';
42
- this._heading.textContent = this._titleText || '';
43
- this._heading.style.display = this._titleText ? '' : 'none';
51
+ const title = this._titleText || '';
52
+ if (this._heading.textContent !== title) this._heading.textContent = title;
53
+ this._heading.style.display = title ? '' : 'none';
54
+ // The end and the whole head leave the layout when they are empty, so a
55
+ // title alone lays out as it always did.
56
+ const end = this._endSlot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.textContent.trim());
57
+ this._end.style.display = end ? '' : 'none';
58
+ this._head.style.display = title || end ? '' : 'none';
44
59
  }
45
60
 
61
+ setupEvents() {
62
+ this._onEnd = () => this._sync();
63
+ this._endSlot.addEventListener('slotchange', this._onEnd);
64
+ }
65
+
66
+ teardownEvents() { this._endSlot?.removeEventListener('slotchange', this._onEnd); }
67
+
46
68
  onAttributeChanged(name, _old, value) {
47
69
  if (name === 'title' && !this.takeTitle(value)) return;
48
70
  if (name === 'href' && (value === null) !== (this._box.localName === 'div')) { this._build(); return; }
@@ -3,13 +3,22 @@ import { NkElement } from '../../base.js';
3
3
  // <nk-panels><nk-panel …></nk-panel><nk-panel …></nk-panel></nk-panels>
4
4
  // → <div class="nk-panels">…</div>: columns of at least 200px that share the
5
5
  // row and fall to one on a phone. The <nk-panel> hosts are display: contents,
6
- // so each .nk-panel is a cell of this grid.
6
+ // so each .nk-panel is a cell of this grid. `flush` (NotionKit 1.12.0) drops
7
+ // the grid's outer margin.
7
8
  class NkPanels extends NkElement {
9
+ static get observedAttributes() { return ['flush']; }
10
+
8
11
  render() {
9
- const grid = this.createElement('div', ['nk-panels']);
10
- grid.appendChild(document.createElement('slot'));
11
- this._wrapper.appendChild(grid);
12
+ this._grid = this.createElement('div', ['nk-panels']);
13
+ this._grid.appendChild(document.createElement('slot'));
14
+ this._wrapper.appendChild(this._grid);
15
+ this.onAttributeChanged();
12
16
  }
17
+
18
+ onAttributeChanged() { this._grid?.classList.toggle('flush', this.getBoolAttr('flush')); }
19
+
20
+ get flush() { return this.getBoolAttr('flush'); }
21
+ set flush(v) { this.setBoolAttr('flush', v); }
13
22
  }
14
23
 
15
24
  customElements.define('nk-panels', NkPanels);
@@ -5,9 +5,11 @@ import { NkElement } from '../../base.js';
5
5
  // The value is the element's content – tags, an avatar and a name, a date, a
6
6
  // <nk-progress wide>. slot="icon" replaces the icon glyph. A click on the
7
7
  // name or the value fires nk-action { action: 'name' | 'value', label }, the
8
- // moment Notion opens the property's editor.
8
+ // moment Notion opens the property's editor. `text` (NotionKit 1.12.0) lets a
9
+ // value that is text – an address, a model name with a tag – flow as text
10
+ // instead of setting its parts one under the other.
9
11
  class NkProp extends NkElement {
10
- static get observedAttributes() { return ['label', 'icon']; }
12
+ static get observedAttributes() { return ['label', 'icon', 'text']; }
11
13
 
12
14
  render() {
13
15
  this._row = this.createElement('div', ['nk-prop']);
@@ -29,6 +31,7 @@ class NkProp extends NkElement {
29
31
  this._icon.textContent = icon || '';
30
32
  this._icon.style.display = icon ? '' : 'none';
31
33
  this._label.data = this.getAttribute('label') || '';
34
+ this._value.classList.toggle('text', this.getBoolAttr('text'));
32
35
  }
33
36
 
34
37
  setupEvents() {
@@ -46,6 +49,8 @@ class NkProp extends NkElement {
46
49
  set label(v) { this.setAttribute('label', v); }
47
50
  get icon() { return this.getAttribute('icon'); }
48
51
  set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
52
+ get text() { return this.getBoolAttr('text'); }
53
+ set text(v) { this.setBoolAttr('text', v); }
49
54
  }
50
55
 
51
56
  customElements.define('nk-prop', NkProp);
@@ -6,13 +6,22 @@ import { NkElement } from '../../base.js';
6
6
  // </nk-props>
7
7
  // → <dl class="nk-props">…</dl> – the properties under a page title. The rows
8
8
  // are <nk-prop> hosts with display: contents, so each .nk-prop is a row of
9
- // this list.
9
+ // this list. `flush` (NotionKit 1.12.0) drops the outer margin meant for the
10
+ // flow under a title – for a list inside a panel or a flex column.
10
11
  class NkProps extends NkElement {
12
+ static get observedAttributes() { return ['flush']; }
13
+
11
14
  render() {
12
- const list = this.createElement('dl', ['nk-props']);
13
- list.appendChild(document.createElement('slot'));
14
- this._wrapper.appendChild(list);
15
+ this._list = this.createElement('dl', ['nk-props']);
16
+ this._list.appendChild(document.createElement('slot'));
17
+ this._wrapper.appendChild(this._list);
18
+ this.onAttributeChanged();
15
19
  }
20
+
21
+ onAttributeChanged() { this._list?.classList.toggle('flush', this.getBoolAttr('flush')); }
22
+
23
+ get flush() { return this.getBoolAttr('flush'); }
24
+ set flush(v) { this.setBoolAttr('flush', v); }
16
25
  }
17
26
 
18
27
  customElements.define('nk-props', NkProps);
@@ -10,9 +10,15 @@ import { NkElement } from '../../base.js';
10
10
  // A setup in a few steps (NotionKit 1.7.0). `current` counts from 1; the
11
11
  // steps before it are done, one past the last marks all of them done. The
12
12
  // `steps` attribute is a comma-separated list; the property also takes
13
- // { label, desc } objects for a line under the label.
13
+ // { label, desc, state, value } objects for a line under the label and a
14
+ // state of its own – 'done', 'skipped' or 'open' (NotionKit 1.12.0) – for a
15
+ // wizard that lets a step be skipped while a later one is done.
16
+ // `selectable` makes each label a button: a click or Enter fires
17
+ // nk-select { index, value, step } – index counts from 1, like `current` –
18
+ // and, unless cancelled, makes that step current. `horizontal` sets the
19
+ // steps in one row above a wizard.
14
20
  class NkSteps extends NkElement {
15
- static get observedAttributes() { return ['steps', 'current', 'label']; }
21
+ static get observedAttributes() { return ['steps', 'current', 'label', 'selectable', 'horizontal']; }
16
22
 
17
23
  render() {
18
24
  this._list = this.createElement('ol', ['nk-steps']);
@@ -25,32 +31,67 @@ class NkSteps extends NkElement {
25
31
  return (this.getAttribute('steps') || '').split(',').map(s => s.trim()).filter(Boolean);
26
32
  }
27
33
 
34
+ _step(i) {
35
+ const step = this._items()[i];
36
+ return typeof step === 'object' && step ? step : step === undefined ? undefined : { label: String(step) };
37
+ }
38
+
28
39
  _sync() {
29
40
  const label = this.getAttribute('label');
30
41
  if (label) this._list.setAttribute('aria-label', label); else this._list.removeAttribute('aria-label');
31
- const current = this.current;
32
- this._list.replaceChildren(...this._items().map((step, i) => {
33
- const n = i + 1, { label: text, desc } = typeof step === 'object' && step ? step : { label: String(step) };
34
- const li = this.createElement('li', ['nk-step']);
35
- li.classList.toggle('done', n < current);
42
+ this._list.classList.toggle('horizontal', this.getBoolAttr('horizontal'));
43
+ const current = this.current, selectable = this.getBoolAttr('selectable');
44
+ // Rebuilding drops the focused label; the new one at that place takes it back.
45
+ const focused = this.shadowRoot?.activeElement?.closest?.('li')?.dataset.index;
46
+ this._list.replaceChildren(...this._items().map((_, i) => {
47
+ const n = i + 1, { label: text, desc, state } = this._step(i);
48
+ // A state of its own wins; without one, the steps before `current` are done.
49
+ const done = state ? state === 'done' : n < current, skipped = state === 'skipped';
50
+ const li = this.createElement('li', ['nk-step'], { 'data-index': String(n) });
51
+ li.classList.toggle('done', done);
52
+ li.classList.toggle('skipped', skipped);
36
53
  li.classList.toggle('current', n === current);
37
54
  if (n === current) li.setAttribute('aria-current', 'step');
38
55
  const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
39
- mark.textContent = n < current ? '✓' : String(n);
40
- const body = document.createElement('span');
56
+ mark.textContent = done ? '✓' : skipped ? '–' : String(n);
57
+ const body = selectable ? this.createElement('button', ['st-label'], { type: 'button' }) : document.createElement('span');
41
58
  body.textContent = text ?? '';
42
59
  if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
43
60
  li.append(mark, body);
44
61
  return li;
45
62
  }));
63
+ if (focused) this._list.querySelector(`li[data-index="${focused}"] .st-label`)?.focus();
64
+ }
65
+
66
+ setupEvents() {
67
+ this._onClick = (e) => {
68
+ if (!this.getBoolAttr('selectable')) return;
69
+ const li = e.target.closest?.('li.nk-step');
70
+ if (li && this._list.contains(li)) this.select(Number(li.dataset.index));
71
+ };
72
+ this._list.addEventListener('click', this._onClick);
46
73
  }
47
74
 
75
+ teardownEvents() { this._list?.removeEventListener('click', this._onClick); }
48
76
  onAttributeChanged() { this._sync(); }
49
77
 
78
+ /** Jumps to step `index` (from 1): fires nk-select { index, value, step } and, unless cancelled, makes it current. */
79
+ select(index) {
80
+ const step = this._step(index - 1);
81
+ if (!step) return false;
82
+ const ok = this.emit('nk-select', { index, value: step.value ?? step.label, step });
83
+ if (ok) this.current = index;
84
+ return ok;
85
+ }
86
+
50
87
  get steps() { return this._items(); }
51
88
  set steps(v) { this._steps = Array.isArray(v) ? v : null; if (this._list) this._sync(); }
52
89
  get current() { const n = parseInt(this.getAttribute('current'), 10); return Number.isFinite(n) ? n : 1; }
53
90
  set current(v) { this.setAttribute('current', String(v)); }
91
+ get selectable() { return this.getBoolAttr('selectable'); }
92
+ set selectable(v) { this.setBoolAttr('selectable', v); }
93
+ get horizontal() { return this.getBoolAttr('horizontal'); }
94
+ set horizontal(v) { this.setBoolAttr('horizontal', v); }
54
95
  /** Moves on to the next step; returns the new current step. */
55
96
  next() { this.current = Math.min(this.current + 1, this._items().length + 1); return this.current; }
56
97
  }
@@ -1,4 +1,5 @@
1
1
  import { NkElement } from '../../base.js';
2
+ import { boxOf } from '../../util/floating.js';
2
3
 
3
4
  // <nk-tabs value="notes">
4
5
  // <nk-tab value="notes">📝 Notes</nk-tab>
@@ -9,10 +10,13 @@ import { NkElement } from '../../base.js';
9
10
  // → <div class="nk-tabs" role="tablist">…</div> + the panels below it.
10
11
  // The tabs keep one tab active and hide every panel whose data-tab does not
11
12
  // match `value` (through the hidden attribute – the panels stay yours).
13
+ // `scroll` (NotionKit 1.12.0) keeps many tabs in one row that scrolls
14
+ // sideways and holds the active tab in view – after the first layout, on
15
+ // every change and when the row's width changes – as <nk-segmented scroll>.
12
16
  const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
13
17
 
14
18
  class NkTabs extends NkElement {
15
- static get observedAttributes() { return ['value']; }
19
+ static get observedAttributes() { return ['value', 'scroll']; }
16
20
 
17
21
  render() {
18
22
  this._strip = this.createElement('div', ['nk-tabs'], { role: 'tablist' });
@@ -40,6 +44,20 @@ class NkTabs extends NkElement {
40
44
  }
41
45
  for (const panel of this.panels) panel.hidden = panel.dataset.tab !== value;
42
46
  this._value = value;
47
+ this._strip.classList.toggle('scroll', this.getBoolAttr('scroll'));
48
+ this._reveal(this._revealed ? 'smooth' : 'instant');
49
+ }
50
+
51
+ /** Scrolls the row the least distance that shows the active tab whole. */
52
+ _reveal(behavior) {
53
+ if (!this.getBoolAttr('scroll') || !this._strip?.clientWidth) return;
54
+ const tab = this.tabs.find(t => t.hasAttribute('active'));
55
+ if (!tab) return;
56
+ const row = this._strip.getBoundingClientRect(), r = boxOf(tab), pad = 8;
57
+ const delta = r.left < row.left ? r.left - row.left - pad : r.right > row.right ? r.right - row.right + pad : 0;
58
+ const smooth = behavior === 'smooth' && !matchMedia('(prefers-reduced-motion: reduce)').matches;
59
+ if (delta) this._strip.scrollBy({ left: delta, behavior: smooth ? 'smooth' : 'instant' });
60
+ this._revealed = true;
43
61
  }
44
62
 
45
63
  setupEvents() {
@@ -58,6 +76,8 @@ class NkTabs extends NkElement {
58
76
  this._tabSlot.addEventListener('slotchange', this._onSlot);
59
77
  this._panelSlot.addEventListener('slotchange', this._onSlot);
60
78
  this.addEventListener('keydown', this._onKey);
79
+ this._resize = new ResizeObserver(() => this._reveal('instant'));
80
+ this._resize.observe(this._strip);
61
81
  this._sync();
62
82
  }
63
83
 
@@ -65,6 +85,7 @@ class NkTabs extends NkElement {
65
85
  this._tabSlot?.removeEventListener('slotchange', this._onSlot);
66
86
  this._panelSlot?.removeEventListener('slotchange', this._onSlot);
67
87
  this.removeEventListener('keydown', this._onKey);
88
+ this._resize?.disconnect();
68
89
  }
69
90
 
70
91
  onAttributeChanged() {
@@ -75,6 +96,8 @@ class NkTabs extends NkElement {
75
96
 
76
97
  get value() { return this._value ?? this.getAttribute('value'); }
77
98
  set value(v) { this.setAttribute('value', v); }
99
+ get scroll() { return this.getBoolAttr('scroll'); }
100
+ set scroll(v) { this.setBoolAttr('scroll', v); }
78
101
  }
79
102
 
80
103
  customElements.define('nk-tabs', NkTabs);
@@ -4,7 +4,12 @@
4
4
  // be styled. Exported for consumers who render tables themselves.
5
5
  //
6
6
  // column: { key, label, type, icon, options: [{ value, label, color }], title }
7
- // types: text | select | multi-select | date | person | checkbox | url | number | progress
7
+ // types: text | select | multi-select | date | person | checkbox | url | number | progress | actions
8
+ // url: a string is an address outside, opened in a new tab; { href, label,
9
+ // target } a link of your own – to another page of the app, with its text.
10
+ // actions: buttons for the row from column.actions [{ action, label, icon,
11
+ // danger, disabled, tooltip }]; the row's value – action names or objects –
12
+ // picks which of them it shows, all of them without one.
8
13
  import { paintAvatar, initialsOf } from './avatar.js';
9
14
 
10
15
  const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
@@ -23,6 +28,7 @@ export function tagFor(column, value) {
23
28
 
24
29
  export function renderPropertyCell(column, value, row = {}) {
25
30
  const type = column.type || 'text';
31
+ if (type === 'actions') return actions(column, value);
26
32
  if (value === undefined || value === null || value === '') {
27
33
  if (type === 'checkbox') return checkbox(column, false, row);
28
34
  if (type === 'progress') return progress(0);
@@ -47,7 +53,14 @@ export function renderPropertyCell(column, value, row = {}) {
47
53
  return cell;
48
54
  }
49
55
  case 'checkbox': return checkbox(column, !!value, row);
50
- case 'url': { const a = el('a', null, String(value).replace(/^https?:\/\//, '')); a.href = String(value); a.target = '_blank'; a.rel = 'noopener'; return a; }
56
+ case 'url': {
57
+ const link = typeof value === 'object' ? value : { href: String(value), label: String(value).replace(/^https?:\/\//, ''), target: '_blank' };
58
+ const a = el('a', null, link.label ?? link.href);
59
+ a.href = link.href ?? '';
60
+ if (link.target) a.target = link.target;
61
+ if (link.target === '_blank') a.rel = 'noopener';
62
+ return a;
63
+ }
51
64
  // column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
52
65
  case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
53
66
  case 'progress': return progress(Number(value) || 0);
@@ -63,6 +76,22 @@ export function renderPropertyCell(column, value, row = {}) {
63
76
  }
64
77
  }
65
78
 
79
+ function actions(column, value) {
80
+ const all = column.actions || [];
81
+ const list = value == null ? all
82
+ : (Array.isArray(value) ? value : [value]).map(v => typeof v === 'string' ? all.find(a => a.action === v) ?? { action: v, label: v } : v);
83
+ const wrap = el('span', 'row-actions');
84
+ for (const a of list) {
85
+ const b = el('button', `nk-btn ${a.danger ? 'danger' : 'secondary'} small`, [a.icon, a.label ?? a.action].filter(Boolean).join(' '));
86
+ b.type = 'button';
87
+ b.dataset.action = a.action;
88
+ b.disabled = !!a.disabled;
89
+ if (a.tooltip) b.dataset.tooltip = a.tooltip;
90
+ wrap.appendChild(b);
91
+ }
92
+ return wrap;
93
+ }
94
+
66
95
  function checkbox(column, checked, row) {
67
96
  const label = el('label', 'nk-check');
68
97
  const input = el('input');