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