@jungherz-de/notionkit-elements 1.5.3 → 1.6.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.
Files changed (103) hide show
  1. package/README.md +25 -16
  2. package/dist/components/base.js +346 -0
  3. package/dist/components/nk-ai-input-row.js +2 -1
  4. package/dist/components/nk-ai-msg.js +10 -3
  5. package/dist/components/nk-ai-thread.js +2 -1
  6. package/dist/components/nk-app.js +2 -1
  7. package/dist/components/nk-avatar-group.js +2 -1
  8. package/dist/components/nk-avatar.js +57 -0
  9. package/dist/components/nk-banner.js +2 -1
  10. package/dist/components/nk-block-host.js +2 -1
  11. package/dist/components/nk-board-view.js +4 -2
  12. package/dist/components/nk-breadcrumb.js +2 -1
  13. package/dist/components/nk-btn.js +2 -1
  14. package/dist/components/nk-callout.js +2 -1
  15. package/dist/components/nk-check.js +2 -1
  16. package/dist/components/nk-cmdk.js +2 -1
  17. package/dist/components/nk-code.js +2 -1
  18. package/dist/components/nk-comment.js +9 -6
  19. package/dist/components/nk-comments.js +2 -1
  20. package/dist/components/nk-danger-zone.js +2 -1
  21. package/dist/components/nk-database.js +2 -1
  22. package/dist/components/nk-divider.js +2 -1
  23. package/dist/components/nk-emoji-picker.js +2 -1
  24. package/dist/components/nk-empty.js +2 -1
  25. package/dist/components/nk-field.js +2 -1
  26. package/dist/components/nk-fields.js +2 -1
  27. package/dist/components/nk-filter-bar.js +2 -1
  28. package/dist/components/nk-heading.js +2 -1
  29. package/dist/components/nk-input.js +2 -1
  30. package/dist/components/nk-kbd.js +2 -1
  31. package/dist/components/nk-list-view.js +96 -0
  32. package/dist/components/nk-member-list.js +2 -1
  33. package/dist/components/nk-member-row.js +10 -6
  34. package/dist/components/nk-mention.js +2 -1
  35. package/dist/components/nk-menu-item.js +23 -3
  36. package/dist/components/nk-menu.js +2 -1
  37. package/dist/components/nk-modal.js +2 -1
  38. package/dist/components/nk-model-card.js +2 -1
  39. package/dist/components/nk-page-actions.js +2 -1
  40. package/dist/components/nk-page-cover.js +12 -11
  41. package/dist/components/nk-page-title.js +2 -1
  42. package/dist/components/nk-page.js +12 -4
  43. package/dist/components/nk-panel.js +61 -0
  44. package/dist/components/nk-panels.js +18 -0
  45. package/dist/components/nk-pop.js +2 -1
  46. package/dist/components/nk-profile-row.js +2 -1
  47. package/dist/components/nk-progress.js +5 -2
  48. package/dist/components/nk-prop.js +54 -0
  49. package/dist/components/nk-props.js +21 -0
  50. package/dist/components/nk-quote.js +2 -1
  51. package/dist/components/nk-radio.js +2 -1
  52. package/dist/components/nk-section-label.js +2 -1
  53. package/dist/components/nk-segmented.js +2 -1
  54. package/dist/components/nk-select.js +2 -1
  55. package/dist/components/nk-settings-pane.js +2 -1
  56. package/dist/components/nk-settings-user.js +2 -1
  57. package/dist/components/nk-sidebar.js +2 -1
  58. package/dist/components/nk-skeleton.js +2 -1
  59. package/dist/components/nk-slider.js +2 -1
  60. package/dist/components/nk-stat.js +2 -1
  61. package/dist/components/nk-stats.js +2 -1
  62. package/dist/components/nk-switch.js +2 -1
  63. package/dist/components/nk-synced.js +2 -1
  64. package/dist/components/nk-tab-bar-item.js +2 -1
  65. package/dist/components/nk-tab-bar.js +2 -1
  66. package/dist/components/nk-tab.js +2 -1
  67. package/dist/components/nk-table-view.js +6 -2
  68. package/dist/components/nk-tabs.js +2 -1
  69. package/dist/components/nk-tag.js +2 -1
  70. package/dist/components/nk-template-btn.js +2 -1
  71. package/dist/components/nk-textarea.js +2 -1
  72. package/dist/components/nk-theme-toggle.js +2 -1
  73. package/dist/components/nk-toast.js +2 -1
  74. package/dist/components/nk-todo.js +2 -1
  75. package/dist/components/nk-toggle.js +2 -1
  76. package/dist/components/nk-topbar.js +2 -1
  77. package/dist/components/nk-tree-item.js +2 -1
  78. package/dist/components/nk-tree.js +2 -1
  79. package/dist/components/nk-workspace-switcher.js +2 -1
  80. package/dist/components/shared/avatar-BiqCaHOt.js +19 -0
  81. package/dist/components/shared/{property-cell-CtXQpUjQ.js → property-cell-o7N30kAQ.js} +8 -3
  82. package/dist/notionkit-elements.esm.js +374 -34
  83. package/dist/notionkit-elements.js +380 -33
  84. package/dist/notionkit-elements.min.js +1 -1
  85. package/package.json +4 -3
  86. package/src/components/content/nk-progress.js +3 -1
  87. package/src/components/data/nk-ai-msg.js +8 -2
  88. package/src/components/data/nk-comment.js +7 -5
  89. package/src/components/data/nk-list-view.js +93 -0
  90. package/src/components/data/nk-table-view.js +2 -0
  91. package/src/components/overlays/nk-menu-item.js +21 -2
  92. package/src/components/page/nk-avatar.js +55 -0
  93. package/src/components/page/nk-member-row.js +8 -5
  94. package/src/components/page/nk-page-cover.js +10 -10
  95. package/src/components/page/nk-page.js +10 -3
  96. package/src/components/page/nk-panel.js +59 -0
  97. package/src/components/page/nk-panels.js +16 -0
  98. package/src/components/page/nk-prop.js +52 -0
  99. package/src/components/page/nk-props.js +19 -0
  100. package/src/index.js +14 -0
  101. package/src/util/avatar.js +17 -0
  102. package/src/util/property-cell.js +7 -3
  103. package/dist/components/shared/base-C3eJwHKA.js +0 -366
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungherz-de/notionkit-elements",
3
- "version": "1.5.3",
3
+ "version": "1.6.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)",
@@ -20,6 +20,7 @@
20
20
  "import": "./dist/notionkit-elements.esm.js",
21
21
  "default": "./dist/notionkit-elements.js"
22
22
  },
23
+ "./base.js": "./dist/components/base.js",
23
24
  "./components/*.js": "./dist/components/*.js",
24
25
  "./package.json": "./package.json"
25
26
  },
@@ -58,10 +59,10 @@
58
59
  "dark-mode"
59
60
  ],
60
61
  "peerDependencies": {
61
- "@jungherz-de/notionkit": ">=1.5.3"
62
+ "@jungherz-de/notionkit": ">=1.6.0"
62
63
  },
63
64
  "devDependencies": {
64
- "@jungherz-de/notionkit": "^1.5.3",
65
+ "@jungherz-de/notionkit": "^1.6.0",
65
66
  "@rollup/plugin-node-resolve": "^15.0.0",
66
67
  "@rollup/plugin-terser": "^0.4.0",
67
68
  "rollup": "^4.0.0",
@@ -1,9 +1,10 @@
1
1
  import { NkElement } from '../../base.js';
2
2
 
3
3
  // <nk-progress value="72" label="72%"></nk-progress>
4
+ // <nk-progress value="72" label="72 %" wide></nk-progress> → fills its row, the label stays beside it
4
5
  // The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
5
6
  class NkProgress extends NkElement {
6
- static get observedAttributes() { return ['value', 'max', 'label']; }
7
+ static get observedAttributes() { return ['value', 'max', 'label', 'wide']; }
7
8
 
8
9
  render() {
9
10
  this._bar = this.createElement('span', ['nk-progress'], { role: 'progressbar' });
@@ -22,6 +23,7 @@ class NkProgress extends NkElement {
22
23
  const max = Number(this.getAttribute('max')) || 100;
23
24
  const value = Math.min(max, Math.max(0, Number(this.getAttribute('value')) || 0));
24
25
  this._fill.style.width = `${(value / max) * 100}%`;
26
+ this._bar.classList.toggle('wide', this.getBoolAttr('wide'));
25
27
  this._bar.setAttribute('aria-valuenow', value);
26
28
  this._bar.setAttribute('aria-valuemin', 0);
27
29
  this._bar.setAttribute('aria-valuemax', max);
@@ -5,16 +5,18 @@ import { NkElement } from '../../base.js';
5
5
  // Two tasks are open …
6
6
  // <button slot="actions" value="copy">📋 Copy</button><button slot="actions" value="retry">↻ Rephrase</button>
7
7
  // </nk-ai-msg>
8
+ // <nk-ai-msg role="user" bubble>What is still open?</nk-ai-msg> → a grey bubble on the right, as in Notion's AI chat
8
9
  // → <div class="nk-ai-msg user"><span class="mini-avatar">MK</span><div class="a-body"><div class="a-name">You</div>…<div class="nk-ai-actions">…</div></div></div>
9
10
  // Action buttons stay in the light DOM (slotted twins) and fire nk-action { action: value }.
10
11
  class NkAiMsg extends NkElement {
11
- static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color']; }
12
+ static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color', 'bubble']; }
12
13
 
13
14
  render() {
14
15
  this._box = this.createElement('div', ['nk-ai-msg']);
15
16
  this._avatar = this.createElement('span', ['mini-avatar']);
16
17
  const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
17
18
  avatarSlot.appendChild(this._avatar);
19
+ this._avatarSlot = avatarSlot;
18
20
  const body = this.createElement('div', ['a-body']);
19
21
  this._name = this.createElement('div', ['a-name']);
20
22
  this._nameText = document.createTextNode('');
@@ -31,12 +33,16 @@ class NkAiMsg extends NkElement {
31
33
 
32
34
  _sync() {
33
35
  const user = this.getAttribute('role') === 'user';
36
+ const bubble = this.getBoolAttr('bubble');
34
37
  this._box.classList.toggle('user', user);
38
+ this._box.classList.toggle('bubble', bubble);
35
39
  const name = this.getAttribute('name');
36
40
  this._nameText.data = name || '';
37
41
  const badge = this.getAttribute('badge');
38
42
  this._badge.textContent = badge ? ` ${badge}` : '';
39
- this._name.style.display = name ? '' : 'none';
43
+ // A bubble stands without avatar and name; the text says who asked.
44
+ this._name.style.display = name && !bubble ? '' : 'none';
45
+ this._avatarSlot.style.display = bubble ? 'none' : '';
40
46
  this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
41
47
  const color = this.getAttribute('color');
42
48
  this._avatar.style.background = color || '';
@@ -1,14 +1,16 @@
1
1
  import { NkElement } from '../../base.js';
2
+ import { paintAvatar, initialsOf } from '../../util/avatar.js';
2
3
 
3
- // <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="#448361">The board view feels close.</nk-comment>
4
- // → <div class="nk-comment"><span class="mini-avatar">SL</span><div><div class="c-head"><b>Sara Lindt</b> · 1 hr ago</div><div class="c-body">…</div></div></div>
4
+ // <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="green">The board view feels close.</nk-comment>
5
+ // → <div class="nk-comment"><span class="nk-avatar green">SL</span><div><div class="c-head"><b>Sara Lindt</b> · 1 hr ago</div><div class="c-body">…</div></div></div>
6
+ // color: one of Notion's nine names, or any CSS background; without it the avatar gradient.
5
7
  // slot="head" adds content after the name (e.g. an <nk-tag>).
6
8
  class NkComment extends NkElement {
7
9
  static get observedAttributes() { return ['author', 'time', 'avatar', 'color']; }
8
10
 
9
11
  render() {
10
12
  const box = this.createElement('div', ['nk-comment']);
11
- this._avatar = this.createElement('span', ['mini-avatar']);
13
+ this._avatar = this.createElement('span', ['nk-avatar']);
12
14
  const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
13
15
  avatarSlot.appendChild(this._avatar);
14
16
  const text = document.createElement('div');
@@ -29,8 +31,8 @@ class NkComment extends NkElement {
29
31
  this._author.textContent = author;
30
32
  const time = this.getAttribute('time');
31
33
  this._time.data = time ? ` · ${time}` : '';
32
- this._avatar.textContent = this.getAttribute('avatar') || author.split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase();
33
- this._avatar.style.background = this.getAttribute('color') || 'var(--nk-text-tertiary)';
34
+ this._avatar.textContent = this.getAttribute('avatar') || initialsOf(author);
35
+ paintAvatar(this._avatar, this.getAttribute('color'));
34
36
  }
35
37
 
36
38
  onAttributeChanged() { this._sync(); }
@@ -0,0 +1,93 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { renderPropertyCell } from '../../util/property-cell.js';
3
+
4
+ // <nk-list-view name="list" label="☰ List" meta-keys="due,status" new-row></nk-list-view>
5
+ // The third database view: one line per row – icon and title, the `meta-keys`
6
+ // on the right (default: the select and date columns, in column order). Dates
7
+ // and text stand as text, selects as tags, a person as avatar and name.
8
+ // Standalone: view.columns = […]; view.rows = […]. Inside <nk-database> the
9
+ // database pushes the data. Rows fire nk-select, the add row nk-action
10
+ // { action: 'new-row' }.
11
+ // → <div class="nk-list"><div class="nk-list-item"><span class="l-icon">…</span><span class="l-title">…</span><span class="l-meta">…</span></div>…</div>
12
+ class NkListView extends NkElement {
13
+ static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'title-key', 'meta-keys', 'new-row', 'new-row-label']; }
14
+
15
+ render() {
16
+ this._box = document.createElement('div');
17
+ this._list = this.createElement('div', ['nk-list'], { role: 'list' });
18
+ this._newRow = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0' });
19
+ this._box.append(this._list, this._newRow);
20
+ this._wrapper.appendChild(this._box);
21
+ this._columns ??= []; this._rows ??= [];
22
+ this._render();
23
+ this.closest('nk-database')?.requestSync?.();
24
+ }
25
+
26
+ _titleColumn() {
27
+ const key = this.getAttribute('title-key');
28
+ return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
29
+ }
30
+ _metaColumns() {
31
+ const keys = (this.getAttribute('meta-keys') || '').split(',').map(s => s.trim()).filter(Boolean);
32
+ if (keys.length) return keys.map(k => this._columns.find(c => c.key === k)).filter(Boolean);
33
+ return this._columns.filter(c => c.type === 'select' || c.type === 'date');
34
+ }
35
+
36
+ _render() {
37
+ if (!this._list) return;
38
+ const title = this._titleColumn(), meta = this._metaColumns();
39
+ this._list.replaceChildren();
40
+ for (const row of this._rows) {
41
+ const item = this.createElement('div', ['nk-list-item'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
42
+ const icon = this.createElement('span', ['l-icon']);
43
+ icon.textContent = row.icon || '';
44
+ if (!row.icon) icon.style.display = 'none';
45
+ const t = this.createElement('span', ['l-title']);
46
+ t.textContent = title ? String(row[title.key] ?? '') : '';
47
+ const m = this.createElement('span', ['l-meta']);
48
+ for (const c of meta) {
49
+ const v = row[c.key];
50
+ if (v === undefined || v === null || v === '') continue;
51
+ m.appendChild(c.type === 'date' || c.type === 'text' || !c.type ? document.createTextNode(String(v)) : renderPropertyCell(c, v, row));
52
+ }
53
+ item.append(icon, t, m);
54
+ this._list.appendChild(item);
55
+ }
56
+ this._newRow.textContent = this.getAttribute('new-row-label') || '+ New page';
57
+ this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
58
+ }
59
+
60
+ setupEvents() {
61
+ this._onClick = (e) => {
62
+ if (this._newRow.contains(e.target)) { this.emit('nk-action', { action: 'new-row' }); return; }
63
+ const item = e.target.closest('.nk-list-item');
64
+ if (item) { const row = this._rowById(item.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
65
+ };
66
+ this._onKey = (e) => {
67
+ if (e.key !== 'Enter' && e.key !== ' ') return;
68
+ if (e.target === this._newRow || e.target.classList?.contains('nk-list-item')) { e.preventDefault(); e.target.click(); }
69
+ };
70
+ this._box.addEventListener('click', this._onClick);
71
+ this._box.addEventListener('keydown', this._onKey);
72
+ }
73
+
74
+ teardownEvents() {
75
+ this._box?.removeEventListener('click', this._onClick);
76
+ this._box?.removeEventListener('keydown', this._onKey);
77
+ }
78
+
79
+ _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
80
+ onAttributeChanged() { this._render(); }
81
+
82
+ get name() { return this.getAttribute('name') || 'list'; }
83
+ get columns() { return this._columns; }
84
+ set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
85
+ get rows() { return this._rows; }
86
+ set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
87
+ set data({ columns, rows }) { this.setData(columns, rows); }
88
+ setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
89
+ refresh() { this._render(); }
90
+ }
91
+
92
+ customElements.define('nk-list-view', NkListView);
93
+ export { NkListView };
@@ -50,6 +50,8 @@ class NkTableView extends NkElement {
50
50
  const r = this.createElement('tr', [], { 'data-id': row.id ?? '' });
51
51
  for (const col of this._columns) {
52
52
  const td = document.createElement('td');
53
+ // Numbers right-aligned in figures of equal width, as Notion sets them.
54
+ if (col.type === 'number') td.className = 'num';
53
55
  td.appendChild(renderPropertyCell(col, row[col.key], row));
54
56
  r.appendChild(td);
55
57
  }
@@ -3,9 +3,13 @@ import { NkElement } from '../../base.js';
3
3
  // <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
4
4
  // <nk-menu-item type="separator"></nk-menu-item> <nk-menu-item type="label">Danger</nk-menu-item>
5
5
  // <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
6
+ // <nk-menu-item type="switch" icon="🔡" value="small" checked>Small text</nk-menu-item>
6
7
  // → <div class="nk-menu-item danger"><span class="m-icon">🗑️</span>Delete<span class="m-shortcut">…</span></div>
8
+ // A switch item carries a .nk-switch on the right, as "Small text" does in
9
+ // Notion's page menu: a click flips `checked` and fires nk-change { value,
10
+ // checked } instead of nk-select, so the menu around it stays open.
7
11
  class NkMenuItem extends NkElement {
8
- static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled']; }
12
+ static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled', 'checked']; }
9
13
 
10
14
  render() { this._build(); }
11
15
 
@@ -18,12 +22,15 @@ class NkMenuItem extends NkElement {
18
22
  el = this.createElement('div', ['nk-menu-label']);
19
23
  el.appendChild(document.createElement('slot'));
20
24
  } else {
21
- el = this.createElement('div', ['nk-menu-item'], { role: 'menuitem', tabindex: '-1' });
25
+ const isSwitch = type === 'switch';
26
+ el = this.createElement('div', ['nk-menu-item'], { role: isSwitch ? 'menuitemcheckbox' : 'menuitem', tabindex: '-1' });
22
27
  this._icon = this.createElement('span', ['m-icon']);
23
28
  const iconSlot = this.createElement('slot', [], { name: 'icon' });
24
29
  iconSlot.appendChild(this._icon);
25
30
  this._shortcut = this.createElement('span', ['m-shortcut']);
26
31
  el.append(iconSlot, document.createElement('slot'), this._shortcut);
32
+ this._switch = isSwitch ? this.createElement('span', ['nk-switch'], { 'aria-hidden': 'true' }) : null;
33
+ if (this._switch) el.appendChild(this._switch);
27
34
  }
28
35
  if (this._el) { this._el.removeEventListener('click', this._onClick); this._el.replaceWith(el); } else this._wrapper.appendChild(el);
29
36
  this._el = el;
@@ -41,6 +48,11 @@ class NkMenuItem extends NkElement {
41
48
  this._shortcut.style.display = sc ? '' : 'none';
42
49
  this._el.classList.toggle('danger', this.getBoolAttr('danger'));
43
50
  this._el.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
51
+ if (this._switch) {
52
+ const on = this.getBoolAttr('checked') ? 'true' : 'false';
53
+ this._el.setAttribute('aria-checked', on);
54
+ this._switch.setAttribute('aria-checked', on);
55
+ }
44
56
  }
45
57
 
46
58
  setupEvents() {
@@ -52,6 +64,11 @@ class NkMenuItem extends NkElement {
52
64
 
53
65
  select() {
54
66
  if (!this._el.classList.contains('nk-menu-item') || this.getBoolAttr('disabled')) return;
67
+ if (this._switch) {
68
+ this.checked = !this.checked;
69
+ this.emit('nk-change', { value: this.value, checked: this.checked, item: this });
70
+ return;
71
+ }
55
72
  this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
56
73
  }
57
74
 
@@ -59,6 +76,8 @@ class NkMenuItem extends NkElement {
59
76
  focus(o) { this._el?.focus(o); }
60
77
  get selectable() { return this._el?.classList.contains('nk-menu-item') && !this.getBoolAttr('disabled'); }
61
78
 
79
+ get checked() { return this.getBoolAttr('checked'); }
80
+ set checked(v) { this.setBoolAttr('checked', v); }
62
81
  get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
63
82
  set value(v) { this.setAttribute('value', v); }
64
83
  }
@@ -0,0 +1,55 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { paintAvatar, initialsOf } from '../../util/avatar.js';
3
+
4
+ // <nk-avatar>MK</nk-avatar> → <span class="nk-avatar">MK</span>
5
+ // <nk-avatar size="small" color="green">SL</nk-avatar>
6
+ // <nk-avatar name="Ada Lovelace" size="large"></nk-avatar> → initials "AL"
7
+ // <nk-avatar src="ada.jpg" name="Ada Lovelace" size="xlarge"></nk-avatar>
8
+ // <nk-avatar square>A</nk-avatar> → a workspace icon
9
+ // size: small (20px) · default 24px · large (32px) · xlarge (56px). color: one
10
+ // of Notion's nine names, or any CSS background. Without content the initials
11
+ // come from `name`; with `src` a photo fills the circle and `name` is its alt.
12
+ class NkAvatar extends NkElement {
13
+ static get observedAttributes() { return ['size', 'color', 'square', 'src', 'name']; }
14
+
15
+ render() {
16
+ this._box = this.createElement('span', ['nk-avatar']);
17
+ this._slot = document.createElement('slot');
18
+ this._initials = document.createTextNode('');
19
+ this._slot.appendChild(this._initials);
20
+ this._img = this.createElement('img', [], { alt: '' });
21
+ this._box.appendChild(this._slot);
22
+ this._wrapper.appendChild(this._box);
23
+ this._sync();
24
+ }
25
+
26
+ _sync() {
27
+ const size = this.getAttribute('size');
28
+ for (const s of ['small', 'large', 'xlarge']) this._box.classList.toggle(s, s === size);
29
+ this._box.classList.toggle('square', this.getBoolAttr('square'));
30
+ paintAvatar(this._box, this.getAttribute('color'));
31
+ const name = this.getAttribute('name');
32
+ this._initials.data = initialsOf(name);
33
+ const src = this.getAttribute('src');
34
+ if (src) {
35
+ this._img.src = src;
36
+ this._img.alt = name || '';
37
+ if (this._slot.isConnected) this._slot.replaceWith(this._img);
38
+ } else if (this._img.isConnected) this._img.replaceWith(this._slot);
39
+ // A picture or initials with a name speak for themselves; a bare glyph is decoration.
40
+ if (name && !src) { this._box.setAttribute('role', 'img'); this._box.setAttribute('aria-label', name); }
41
+ else { this._box.removeAttribute('role'); this._box.removeAttribute('aria-label'); }
42
+ }
43
+
44
+ onAttributeChanged() { this._sync(); }
45
+
46
+ get size() { return this.getAttribute('size'); }
47
+ set size(v) { v == null ? this.removeAttribute('size') : this.setAttribute('size', v); }
48
+ get color() { return this.getAttribute('color'); }
49
+ set color(v) { v == null ? this.removeAttribute('color') : this.setAttribute('color', v); }
50
+ get src() { return this.getAttribute('src'); }
51
+ set src(v) { v == null ? this.removeAttribute('src') : this.setAttribute('src', v); }
52
+ }
53
+
54
+ customElements.define('nk-avatar', NkAvatar);
55
+ export { NkAvatar };
@@ -1,15 +1,18 @@
1
1
  import { NkElement } from '../../base.js';
2
+ import { paintAvatar, initialsOf } from '../../util/avatar.js';
2
3
 
3
- // <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="#448361">
4
+ // <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="green">
4
5
  // <nk-select slot="role" compact>…</nk-select>
5
6
  // </nk-member-row>
6
- // → <div class="nk-member-row"><span class="mini-avatar">SL</span><div>Sara Lindt<div class="m-mail">…</div></div><select …></div>
7
+ // → <div class="nk-member-row"><span class="nk-avatar green">SL</span><div>Sara Lindt<div class="m-mail">…</div></div><select …></div>
8
+ // color: one of Notion's nine names, or any CSS background; without it the
9
+ // avatar gradient. The row sizes the avatar to 28px.
7
10
  class NkMemberRow extends NkElement {
8
11
  static get observedAttributes() { return ['name', 'mail', 'avatar', 'color', 'last']; }
9
12
 
10
13
  render() {
11
14
  this._row = this.createElement('div', ['nk-member-row']);
12
- this._avatar = this.createElement('span', ['mini-avatar']);
15
+ this._avatar = this.createElement('span', ['nk-avatar']);
13
16
  const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
14
17
  avatarSlot.appendChild(this._avatar);
15
18
  const text = document.createElement('div');
@@ -33,8 +36,8 @@ class NkMemberRow extends NkElement {
33
36
  this._mail.textContent = mail || '';
34
37
  this._mail.style.display = mail ? '' : 'none';
35
38
  const avatar = this.getAttribute('avatar');
36
- this._avatar.textContent = avatar || (this.getAttribute('name') || '').split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase();
37
- this._avatar.style.background = this.getAttribute('color') || 'var(--nk-text-tertiary)';
39
+ this._avatar.textContent = avatar || initialsOf(this.getAttribute('name'));
40
+ paintAvatar(this._avatar, this.getAttribute('color'));
38
41
  this._row.classList.toggle('last', this.getBoolAttr('last'));
39
42
  }
40
43
 
@@ -1,12 +1,16 @@
1
1
  import { NkElement } from '../../base.js';
2
2
 
3
3
  // <nk-page-cover slot="cover"></nk-page-cover> → the token gradient
4
- // <nk-page-cover slot="cover" src="cover.jpg"></nk-page-cover> → an image
4
+ // <nk-page-cover slot="cover" src="cover.jpg" position="center 30%"></nk-page-cover> → a picture
5
+ // → <div class="nk-cover"><img src="cover.jpg" alt=""></div>: the picture fills
6
+ // the band, cropped rather than stretched; `position` moves the crop, as
7
+ // Notion's "Reposition" does. The gradient stays underneath while it loads.
5
8
  class NkPageCover extends NkElement {
6
- static get observedAttributes() { return ['src']; }
9
+ static get observedAttributes() { return ['src', 'position']; }
7
10
 
8
11
  render() {
9
12
  this._cover = this.createElement('div', ['nk-cover']);
13
+ this._img = this.createElement('img', [], { alt: '' });
10
14
  this._wrapper.appendChild(this._cover);
11
15
  this._sync();
12
16
  }
@@ -14,14 +18,10 @@ class NkPageCover extends NkElement {
14
18
  _sync() {
15
19
  const src = this.getAttribute('src');
16
20
  if (src) {
17
- this._cover.style.backgroundImage = `url("${src}")`;
18
- this._cover.style.backgroundSize = 'cover';
19
- this._cover.style.backgroundPosition = 'center';
20
- } else {
21
- this._cover.style.backgroundImage = '';
22
- this._cover.style.backgroundSize = '';
23
- this._cover.style.backgroundPosition = '';
24
- }
21
+ this._img.src = src;
22
+ this._img.style.objectPosition = this.getAttribute('position') || '';
23
+ if (!this._img.isConnected) this._cover.appendChild(this._img);
24
+ } else this._img.remove();
25
25
  }
26
26
 
27
27
  onAttributeChanged() { this._sync(); }
@@ -1,6 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
2
 
3
- // <nk-page icon="🚀" cover>
3
+ // <nk-page icon="🚀" cover full small>
4
4
  // <nk-page-title>NotionKit MVP</nk-page-title>
5
5
  // <nk-page-actions>…</nk-page-actions>
6
6
  // <p class="lead">…</p>
@@ -12,9 +12,10 @@ import { NkElement } from '../../base.js';
12
12
  // The host is display:contents, so .nk-page-scroll is the flex child of
13
13
  // .nk-main that scrolls. `narrow` drops the scroll wrapper for pages that are
14
14
  // the document itself (landing / docs skeleton). The page icon is rendered
15
- // here because its ::slotted twin is keyed on the parent (.nk-page).
15
+ // here because its ::slotted twin is keyed on the parent (.nk-page). `full` and
16
+ // `small` are Notion's page options – full width, 14px text – set on the page.
16
17
  class NkPage extends NkElement {
17
- static get observedAttributes() { return ['icon', 'cover', 'narrow']; }
18
+ static get observedAttributes() { return ['icon', 'cover', 'narrow', 'full', 'small']; }
18
19
 
19
20
  render() {
20
21
  this._page = this.createElement('div', ['nk-page']);
@@ -51,6 +52,8 @@ class NkPage extends NkElement {
51
52
  // (attribute or a slotted nk-page-cover) the icon sits in the padding.
52
53
  const slotted = this._coverSlot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.data.trim());
53
54
  this._page.classList.toggle('covered', this.getBoolAttr('cover') || slotted);
55
+ this._page.classList.toggle('full', this.getBoolAttr('full'));
56
+ this._page.classList.toggle('small', this.getBoolAttr('small'));
54
57
  }
55
58
 
56
59
  setupEvents() {
@@ -72,6 +75,10 @@ class NkPage extends NkElement {
72
75
  set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
73
76
  get cover() { return this.getBoolAttr('cover'); }
74
77
  set cover(v) { this.setBoolAttr('cover', v); }
78
+ get full() { return this.getBoolAttr('full'); }
79
+ set full(v) { this.setBoolAttr('full', v); }
80
+ get small() { return this.getBoolAttr('small'); }
81
+ set small(v) { this.setBoolAttr('small', v); }
75
82
  }
76
83
 
77
84
  customElements.define('nk-page', NkPage);
@@ -0,0 +1,59 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-panel title="Weekly review"><p>Three pages changed.</p></nk-panel>
4
+ // <nk-panel href="/roadmap" cover="covers/aurora.svg" icon="🚀" title="Roadmap"><p>2 min ago</p></nk-panel>
5
+ // → <a class="nk-panel" href="/roadmap"><div class="nk-cover"><img src="…" alt=""></div>
6
+ // <div class="nk-page-icon">🚀</div><h3>Roadmap</h3>…</a>
7
+ // `cover` without a value draws the token gradient, with one a picture; `icon`
8
+ // overlaps the cover as on a page – a page tile, as on Notion's Home. With
9
+ // `href` the panel is a link. `title` is the heading, never a tooltip.
10
+ class NkPanel extends NkElement {
11
+ static get observedAttributes() { return ['title', 'icon', 'cover', 'href', 'target']; }
12
+
13
+ render() {
14
+ this._cover = this.createElement('div', ['nk-cover']);
15
+ this._img = this.createElement('img', [], { alt: '' });
16
+ this._icon = this.createElement('div', ['nk-page-icon']);
17
+ this._heading = document.createElement('h3');
18
+ this._slot = document.createElement('slot');
19
+ this.takeTitle();
20
+ this._build();
21
+ }
22
+
23
+ _build() {
24
+ const href = this.getAttribute('href');
25
+ const box = this.createElement(href ? 'a' : 'div', ['nk-panel']);
26
+ box.append(this._cover, this._icon, this._heading, this._slot);
27
+ this._wrapper.replaceChildren(box);
28
+ this._box = box;
29
+ this._sync();
30
+ }
31
+
32
+ _sync() {
33
+ const href = this.getAttribute('href');
34
+ if (href) { this._box.setAttribute('href', href); this._box.target = this.getAttribute('target') || ''; }
35
+ const cover = this.getAttribute('cover');
36
+ this._cover.style.display = cover === null ? 'none' : '';
37
+ if (cover) { this._img.src = cover; if (!this._img.isConnected) this._cover.appendChild(this._img); }
38
+ else this._img.remove();
39
+ const icon = this.getAttribute('icon');
40
+ this._icon.textContent = icon || '';
41
+ this._icon.style.display = icon ? '' : 'none';
42
+ this._heading.textContent = this._titleText || '';
43
+ this._heading.style.display = this._titleText ? '' : 'none';
44
+ }
45
+
46
+ onAttributeChanged(name, _old, value) {
47
+ if (name === 'title' && !this.takeTitle(value)) return;
48
+ if (name === 'href' && (value === null) !== (this._box.localName === 'div')) { this._build(); return; }
49
+ this._sync();
50
+ }
51
+
52
+ get title() { return this._titleText ?? ''; }
53
+ set title(v) { this._titleText = v == null ? '' : String(v); if (this._initialized) this._sync(); }
54
+ get href() { return this.getAttribute('href'); }
55
+ set href(v) { v == null ? this.removeAttribute('href') : this.setAttribute('href', v); }
56
+ }
57
+
58
+ customElements.define('nk-panel', NkPanel);
59
+ export { NkPanel };
@@ -0,0 +1,16 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-panels><nk-panel …></nk-panel><nk-panel …></nk-panel></nk-panels>
4
+ // → <div class="nk-panels">…</div>: columns of at least 200px that share the
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.
7
+ class NkPanels extends NkElement {
8
+ render() {
9
+ const grid = this.createElement('div', ['nk-panels']);
10
+ grid.appendChild(document.createElement('slot'));
11
+ this._wrapper.appendChild(grid);
12
+ }
13
+ }
14
+
15
+ customElements.define('nk-panels', NkPanels);
16
+ export { NkPanels };
@@ -0,0 +1,52 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-prop label="Due" icon="📅">2 June 2026</nk-prop>
4
+ // → <div class="nk-prop"><dt class="p-name"><span class="p-icon">📅</span>Due</dt><dd class="p-value">2 June 2026</dd></div>
5
+ // The value is the element's content – tags, an avatar and a name, a date, a
6
+ // <nk-progress wide>. slot="icon" replaces the icon glyph. A click on the
7
+ // name or the value fires nk-action { action: 'name' | 'value', label }, the
8
+ // moment Notion opens the property's editor.
9
+ class NkProp extends NkElement {
10
+ static get observedAttributes() { return ['label', 'icon']; }
11
+
12
+ render() {
13
+ this._row = this.createElement('div', ['nk-prop']);
14
+ this._name = this.createElement('dt', ['p-name']);
15
+ this._icon = this.createElement('span', ['p-icon']);
16
+ const iconSlot = this.createElement('slot', [], { name: 'icon' });
17
+ iconSlot.appendChild(this._icon);
18
+ this._label = document.createTextNode('');
19
+ this._name.append(iconSlot, this._label);
20
+ this._value = this.createElement('dd', ['p-value']);
21
+ this._value.appendChild(document.createElement('slot'));
22
+ this._row.append(this._name, this._value);
23
+ this._wrapper.appendChild(this._row);
24
+ this._sync();
25
+ }
26
+
27
+ _sync() {
28
+ const icon = this.getAttribute('icon');
29
+ this._icon.textContent = icon || '';
30
+ this._icon.style.display = icon ? '' : 'none';
31
+ this._label.data = this.getAttribute('label') || '';
32
+ }
33
+
34
+ setupEvents() {
35
+ this._onClick = (e) => {
36
+ const part = e.composedPath().includes(this._name) ? 'name' : 'value';
37
+ this.emit('nk-action', { action: part, label: this.label });
38
+ };
39
+ this._row.addEventListener('click', this._onClick);
40
+ }
41
+
42
+ teardownEvents() { this._row?.removeEventListener('click', this._onClick); }
43
+ onAttributeChanged() { this._sync(); }
44
+
45
+ get label() { return this.getAttribute('label') || ''; }
46
+ set label(v) { this.setAttribute('label', v); }
47
+ get icon() { return this.getAttribute('icon'); }
48
+ set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
49
+ }
50
+
51
+ customElements.define('nk-prop', NkProp);
52
+ export { NkProp };
@@ -0,0 +1,19 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-props>
4
+ // <nk-prop label="Status" icon="◉"><nk-tag color="blue">In progress</nk-tag></nk-prop>
5
+ // <nk-prop label="Owner" icon="👤"><nk-avatar size="small">MK</nk-avatar>Marcel Karas</nk-prop>
6
+ // </nk-props>
7
+ // → <dl class="nk-props">…</dl> – the properties under a page title. The rows
8
+ // are <nk-prop> hosts with display: contents, so each .nk-prop is a row of
9
+ // this list.
10
+ class NkProps extends NkElement {
11
+ render() {
12
+ const list = this.createElement('dl', ['nk-props']);
13
+ list.appendChild(document.createElement('slot'));
14
+ this._wrapper.appendChild(list);
15
+ }
16
+ }
17
+
18
+ customElements.define('nk-props', NkProps);
19
+ export { NkProps };