@jungherz-de/notionkit-elements 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/dist/components/nk-ai-input-row.js +57 -0
  4. package/dist/components/nk-ai-msg.js +67 -0
  5. package/dist/components/nk-ai-thread.js +14 -0
  6. package/dist/components/nk-app.js +23 -0
  7. package/dist/components/nk-avatar-group.js +35 -0
  8. package/dist/components/nk-banner.js +30 -0
  9. package/dist/components/nk-block-host.js +35 -0
  10. package/dist/components/nk-board-view.js +129 -0
  11. package/dist/components/nk-breadcrumb.js +78 -0
  12. package/dist/components/nk-btn.js +107 -0
  13. package/dist/components/nk-callout.js +36 -0
  14. package/dist/components/nk-check.js +77 -0
  15. package/dist/components/nk-cmdk.js +221 -0
  16. package/dist/components/nk-code.js +66 -0
  17. package/dist/components/nk-comment.js +41 -0
  18. package/dist/components/nk-comments.js +57 -0
  19. package/dist/components/nk-danger-zone.js +27 -0
  20. package/dist/components/nk-database.js +100 -0
  21. package/dist/components/nk-divider.js +12 -0
  22. package/dist/components/nk-emoji-picker.js +112 -0
  23. package/dist/components/nk-empty.js +36 -0
  24. package/dist/components/nk-field.js +50 -0
  25. package/dist/components/nk-filter-bar.js +78 -0
  26. package/dist/components/nk-heading.js +29 -0
  27. package/dist/components/nk-input.js +108 -0
  28. package/dist/components/nk-kbd.js +15 -0
  29. package/dist/components/nk-member-list.js +30 -0
  30. package/dist/components/nk-member-row.js +46 -0
  31. package/dist/components/nk-mention.js +31 -0
  32. package/dist/components/nk-menu-item.js +68 -0
  33. package/dist/components/nk-menu.js +45 -0
  34. package/dist/components/nk-modal.js +166 -0
  35. package/dist/components/nk-model-card.js +89 -0
  36. package/dist/components/nk-page-actions.js +15 -0
  37. package/dist/components/nk-page-cover.js +35 -0
  38. package/dist/components/nk-page-title.js +71 -0
  39. package/dist/components/nk-page.js +71 -0
  40. package/dist/components/nk-pop.js +90 -0
  41. package/dist/components/nk-profile-row.js +25 -0
  42. package/dist/components/nk-progress.js +43 -0
  43. package/dist/components/nk-quote.js +32 -0
  44. package/dist/components/nk-radio.js +160 -0
  45. package/dist/components/nk-section-label.js +42 -0
  46. package/dist/components/nk-segmented.js +99 -0
  47. package/dist/components/nk-select.js +127 -0
  48. package/dist/components/nk-settings-pane.js +40 -0
  49. package/dist/components/nk-settings-user.js +36 -0
  50. package/dist/components/nk-sidebar.js +81 -0
  51. package/dist/components/nk-skeleton.js +31 -0
  52. package/dist/components/nk-slider.js +85 -0
  53. package/dist/components/nk-stat.js +40 -0
  54. package/dist/components/nk-stats.js +14 -0
  55. package/dist/components/nk-switch.js +69 -0
  56. package/dist/components/nk-synced.js +26 -0
  57. package/dist/components/nk-tab.js +54 -0
  58. package/dist/components/nk-table-view.js +109 -0
  59. package/dist/components/nk-tabs.js +82 -0
  60. package/dist/components/nk-tag.js +32 -0
  61. package/dist/components/nk-template-btn.js +43 -0
  62. package/dist/components/nk-textarea.js +95 -0
  63. package/dist/components/nk-theme-toggle.js +67 -0
  64. package/dist/components/nk-toast.js +51 -0
  65. package/dist/components/nk-todo.js +73 -0
  66. package/dist/components/nk-toggle.js +56 -0
  67. package/dist/components/nk-topbar.js +24 -0
  68. package/dist/components/nk-tree-item.js +155 -0
  69. package/dist/components/nk-tree.js +123 -0
  70. package/dist/components/nk-workspace-switcher.js +82 -0
  71. package/dist/components/shared/base-6gEcoatG.js +317 -0
  72. package/dist/components/shared/focus-DXrK9oLe.js +47 -0
  73. package/dist/components/shared/property-cell-Dn91uFkf.js +92 -0
  74. package/dist/notionkit-elements.esm.js +4639 -0
  75. package/dist/notionkit-elements.js +4718 -0
  76. package/dist/notionkit-elements.min.js +1 -0
  77. package/package.json +72 -0
  78. package/src/base.js +296 -0
  79. package/src/components/content/nk-callout.js +35 -0
  80. package/src/components/content/nk-code.js +51 -0
  81. package/src/components/content/nk-divider.js +11 -0
  82. package/src/components/content/nk-heading.js +28 -0
  83. package/src/components/content/nk-kbd.js +14 -0
  84. package/src/components/content/nk-progress.js +42 -0
  85. package/src/components/content/nk-quote.js +31 -0
  86. package/src/components/content/nk-tag.js +31 -0
  87. package/src/components/content/nk-todo.js +72 -0
  88. package/src/components/content/nk-toggle.js +55 -0
  89. package/src/components/data/nk-ai-input-row.js +56 -0
  90. package/src/components/data/nk-ai-msg.js +66 -0
  91. package/src/components/data/nk-ai-thread.js +13 -0
  92. package/src/components/data/nk-board-view.js +128 -0
  93. package/src/components/data/nk-comment.js +40 -0
  94. package/src/components/data/nk-comments.js +56 -0
  95. package/src/components/data/nk-database.js +99 -0
  96. package/src/components/data/nk-filter-bar.js +77 -0
  97. package/src/components/data/nk-table-view.js +108 -0
  98. package/src/components/forms/nk-btn.js +106 -0
  99. package/src/components/forms/nk-check.js +76 -0
  100. package/src/components/forms/nk-field.js +49 -0
  101. package/src/components/forms/nk-input.js +107 -0
  102. package/src/components/forms/nk-radio.js +159 -0
  103. package/src/components/forms/nk-select.js +126 -0
  104. package/src/components/forms/nk-slider.js +84 -0
  105. package/src/components/forms/nk-switch.js +68 -0
  106. package/src/components/forms/nk-textarea.js +94 -0
  107. package/src/components/overlays/nk-cmdk.js +201 -0
  108. package/src/components/overlays/nk-emoji-picker.js +94 -0
  109. package/src/components/overlays/nk-menu-item.js +67 -0
  110. package/src/components/overlays/nk-menu.js +44 -0
  111. package/src/components/overlays/nk-modal.js +165 -0
  112. package/src/components/overlays/nk-pop.js +89 -0
  113. package/src/components/overlays/nk-settings-pane.js +39 -0
  114. package/src/components/overlays/nk-settings-user.js +35 -0
  115. package/src/components/overlays/nk-toast.js +50 -0
  116. package/src/components/page/nk-avatar-group.js +34 -0
  117. package/src/components/page/nk-banner.js +29 -0
  118. package/src/components/page/nk-block-host.js +34 -0
  119. package/src/components/page/nk-danger-zone.js +26 -0
  120. package/src/components/page/nk-empty.js +35 -0
  121. package/src/components/page/nk-member-list.js +29 -0
  122. package/src/components/page/nk-member-row.js +45 -0
  123. package/src/components/page/nk-mention.js +30 -0
  124. package/src/components/page/nk-model-card.js +88 -0
  125. package/src/components/page/nk-page-actions.js +14 -0
  126. package/src/components/page/nk-page-cover.js +34 -0
  127. package/src/components/page/nk-page-title.js +70 -0
  128. package/src/components/page/nk-page.js +70 -0
  129. package/src/components/page/nk-profile-row.js +24 -0
  130. package/src/components/page/nk-segmented.js +98 -0
  131. package/src/components/page/nk-skeleton.js +30 -0
  132. package/src/components/page/nk-stat.js +39 -0
  133. package/src/components/page/nk-stats.js +13 -0
  134. package/src/components/page/nk-synced.js +25 -0
  135. package/src/components/page/nk-tab.js +53 -0
  136. package/src/components/page/nk-tabs.js +81 -0
  137. package/src/components/page/nk-template-btn.js +42 -0
  138. package/src/components/shell/nk-app.js +22 -0
  139. package/src/components/shell/nk-breadcrumb.js +77 -0
  140. package/src/components/shell/nk-section-label.js +41 -0
  141. package/src/components/shell/nk-sidebar.js +80 -0
  142. package/src/components/shell/nk-theme-toggle.js +66 -0
  143. package/src/components/shell/nk-topbar.js +23 -0
  144. package/src/components/shell/nk-tree-item.js +154 -0
  145. package/src/components/shell/nk-tree.js +122 -0
  146. package/src/components/shell/nk-workspace-switcher.js +81 -0
  147. package/src/index.js +88 -0
  148. package/src/util/emoji.js +17 -0
  149. package/src/util/focus.js +45 -0
  150. package/src/util/fuzzy.js +19 -0
  151. package/src/util/highlight.js +14 -0
  152. package/src/util/property-cell.js +90 -0
@@ -0,0 +1,56 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-ai-input-row placeholder="Ask Mona something …"></nk-ai-input-row>
4
+ // → <div class="nk-ai-input-row"><span>✨</span><input><button class="nk-ai-send">↑</button></div>
5
+ // Enter or the send button fires nk-submit { text } and clears the field.
6
+ class NkAiInputRow extends NkElement {
7
+ static get observedAttributes() { return ['placeholder', 'value', 'disabled', 'icon']; }
8
+
9
+ render() {
10
+ this._row = this.createElement('div', ['nk-ai-input-row']);
11
+ this._icon = document.createElement('span');
12
+ this._icon.style.fontSize = '14px';
13
+ this._input = this.createElement('input', [], { type: 'text', 'aria-label': 'Message' });
14
+ this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button', 'aria-label': 'Send' });
15
+ this._btn.textContent = '↑';
16
+ this._row.append(this._icon, this._input, this._btn);
17
+ this._wrapper.appendChild(this._row);
18
+ this._sync();
19
+ }
20
+
21
+ _sync() {
22
+ this._input.placeholder = this.getAttribute('placeholder') || 'Ask something …';
23
+ if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
24
+ this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
25
+ const icon = this.getAttribute('icon') ?? '✨';
26
+ this._icon.textContent = icon;
27
+ this._icon.style.display = icon ? '' : 'none';
28
+ }
29
+
30
+ setupEvents() {
31
+ this._onSend = () => this.submit();
32
+ this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this.submit(); } };
33
+ this._btn.addEventListener('click', this._onSend);
34
+ this._input.addEventListener('keydown', this._onKey);
35
+ }
36
+
37
+ teardownEvents() {
38
+ this._btn?.removeEventListener('click', this._onSend);
39
+ this._input?.removeEventListener('keydown', this._onKey);
40
+ }
41
+
42
+ submit() {
43
+ const text = this._input.value.trim();
44
+ if (!text || this.getBoolAttr('disabled')) return;
45
+ const ok = this.emit('nk-submit', { text });
46
+ if (ok) this._input.value = '';
47
+ }
48
+
49
+ onAttributeChanged() { this._sync(); }
50
+ focus(o) { this._input?.focus(o); }
51
+ get value() { return this._input?.value ?? ''; }
52
+ set value(v) { if (this._input) this._input.value = v ?? ''; }
53
+ }
54
+
55
+ customElements.define('nk-ai-input-row', NkAiInputRow);
56
+ export { NkAiInputRow };
@@ -0,0 +1,66 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-ai-msg role="user" name="You" avatar="MK">Summarise the open tasks.</nk-ai-msg>
4
+ // <nk-ai-msg role="assistant" name="Mona" badge="· AI" avatar="✨">
5
+ // Two tasks are open …
6
+ // <button slot="actions" value="copy">📋 Copy</button><button slot="actions" value="retry">↻ Rephrase</button>
7
+ // </nk-ai-msg>
8
+ // → <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
+ // Action buttons stay in the light DOM (slotted twins) and fire nk-action { action: value }.
10
+ class NkAiMsg extends NkElement {
11
+ static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color']; }
12
+
13
+ render() {
14
+ this._box = this.createElement('div', ['nk-ai-msg']);
15
+ this._avatar = this.createElement('span', ['mini-avatar']);
16
+ const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
17
+ avatarSlot.appendChild(this._avatar);
18
+ const body = this.createElement('div', ['a-body']);
19
+ this._name = this.createElement('div', ['a-name']);
20
+ this._nameText = document.createTextNode('');
21
+ this._badge = document.createElement('span');
22
+ this._name.append(this._nameText, this._badge);
23
+ this._actions = this.createElement('div', ['nk-ai-actions']);
24
+ this._actionSlot = this.createElement('slot', [], { name: 'actions' });
25
+ this._actions.appendChild(this._actionSlot);
26
+ body.append(this._name, document.createElement('slot'), this._actions);
27
+ this._box.append(avatarSlot, body);
28
+ this._wrapper.appendChild(this._box);
29
+ this._sync();
30
+ }
31
+
32
+ _sync() {
33
+ const user = this.getAttribute('role') === 'user';
34
+ this._box.classList.toggle('user', user);
35
+ const name = this.getAttribute('name');
36
+ this._nameText.data = name || '';
37
+ const badge = this.getAttribute('badge');
38
+ this._badge.textContent = badge ? ` ${badge}` : '';
39
+ this._name.style.display = name ? '' : 'none';
40
+ this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
41
+ const color = this.getAttribute('color');
42
+ this._avatar.style.background = color || '';
43
+ this._actions.style.display = this._actionSlot.assignedElements().length ? '' : 'none';
44
+ }
45
+
46
+ setupEvents() {
47
+ this._onSlot = () => this._sync();
48
+ this._onClick = (e) => {
49
+ const btn = e.target.closest?.('button');
50
+ if (btn && this._actionSlot.assignedElements().includes(btn)) this.emit('nk-action', { action: btn.value || btn.textContent.trim(), value: btn.value || btn.textContent.trim() });
51
+ };
52
+ this._actionSlot.addEventListener('slotchange', this._onSlot);
53
+ this.addEventListener('click', this._onClick);
54
+ this._sync();
55
+ }
56
+
57
+ teardownEvents() {
58
+ this._actionSlot?.removeEventListener('slotchange', this._onSlot);
59
+ this.removeEventListener('click', this._onClick);
60
+ }
61
+
62
+ onAttributeChanged() { this._sync(); }
63
+ }
64
+
65
+ customElements.define('nk-ai-msg', NkAiMsg);
66
+ export { NkAiMsg };
@@ -0,0 +1,13 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-ai-thread><nk-ai-msg …>…</nk-ai-msg>…</nk-ai-thread> → <div class="nk-ai-thread">
4
+ class NkAiThread extends NkElement {
5
+ render() {
6
+ const box = this.createElement('div', ['nk-ai-thread'], { role: 'log', 'aria-live': 'polite' });
7
+ box.appendChild(document.createElement('slot'));
8
+ this._wrapper.appendChild(box);
9
+ }
10
+ }
11
+
12
+ customElements.define('nk-ai-thread', NkAiThread);
13
+ export { NkAiThread };
@@ -0,0 +1,128 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { renderPropertyCell, tagFor } from '../../util/property-cell.js';
3
+
4
+ // <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
5
+ // Groups rows by a select column (default: the first select column); one
6
+ // column per option, in option order. Cards show the title column and the
7
+ // `meta-keys` (default: date and progress columns). Drag a card onto another
8
+ // column to change its value – the row is updated and nk-change fires.
9
+ // → <div class="nk-board active"><div class="nk-board-col">…</div>…</div>
10
+ class NkBoardView extends NkElement {
11
+ static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'group-by', 'title-key', 'meta-keys', 'new-row']; }
12
+
13
+ render() {
14
+ this._board = this.createElement('div', ['nk-board', 'active']);
15
+ this._wrapper.appendChild(this._board);
16
+ this._columns ??= []; this._rows ??= [];
17
+ this._render();
18
+ this.closest('nk-database')?.requestSync?.();
19
+ }
20
+
21
+ _groupColumn() {
22
+ const key = this.getAttribute('group-by');
23
+ return this._columns.find(c => c.key === key) || this._columns.find(c => c.type === 'select') || null;
24
+ }
25
+ _titleColumn() {
26
+ const key = this.getAttribute('title-key');
27
+ return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
28
+ }
29
+ _metaColumns() {
30
+ const keys = (this.getAttribute('meta-keys') || '').split(',').map(s => s.trim()).filter(Boolean);
31
+ if (keys.length) return keys.map(k => this._columns.find(c => c.key === k)).filter(Boolean);
32
+ return this._columns.filter(c => c.type === 'date' || c.type === 'progress');
33
+ }
34
+
35
+ _render() {
36
+ if (!this._board) return;
37
+ this._board.replaceChildren();
38
+ const group = this._groupColumn(), title = this._titleColumn();
39
+ if (!group) return;
40
+ const options = group.options?.length ? group.options : [...new Set(this._rows.map(r => r[group.key]))].map(v => ({ value: v, label: String(v) }));
41
+ for (const opt of options) {
42
+ const col = this.createElement('div', ['nk-board-col'], { 'data-value': opt.value });
43
+ const header = this.createElement('div', ['nk-board-col-header']);
44
+ const cards = this._rows.filter(r => r[group.key] === opt.value || r[group.key] === opt.label);
45
+ const count = this.createElement('span', ['count']);
46
+ count.textContent = String(cards.length);
47
+ header.append(tagFor(group, opt.value), count);
48
+ col.appendChild(header);
49
+ for (const row of cards) {
50
+ const card = this.createElement('div', ['nk-card'], { draggable: 'true', 'data-id': row.id ?? '', tabindex: '0' });
51
+ const t = this.createElement('div', ['card-title']);
52
+ t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? row[title.key] ?? '' : ''}`;
53
+ const meta = this.createElement('div', ['card-meta']);
54
+ for (const c of this._metaColumns()) {
55
+ const v = row[c.key];
56
+ if (v === undefined || v === null || v === '') continue;
57
+ const s = document.createElement('span');
58
+ if (c.type === 'progress') s.textContent = `▰ ${v}%`;
59
+ else if (c.type === 'date') s.textContent = `📅 ${v}`;
60
+ else s.appendChild(renderPropertyCell(c, v, row));
61
+ meta.appendChild(s);
62
+ }
63
+ card.append(t, meta);
64
+ col.appendChild(card);
65
+ }
66
+ if (this.getBoolAttr('new-row')) {
67
+ const add = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0', 'data-add': opt.value });
68
+ add.style.padding = '6px 10px';
69
+ add.textContent = '+';
70
+ col.appendChild(add);
71
+ }
72
+ this._board.appendChild(col);
73
+ }
74
+ }
75
+
76
+ setupEvents() {
77
+ this._onClick = (e) => {
78
+ const add = e.target.closest('[data-add]');
79
+ if (add) { this.emit('nk-action', { action: 'new-row', value: add.dataset.add }); return; }
80
+ const card = e.target.closest('.nk-card');
81
+ if (card) { const row = this._rowById(card.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
82
+ };
83
+ this._onDragStart = (e) => { const card = e.target.closest?.('.nk-card'); if (!card) return; this._dragId = card.dataset.id; e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', card.dataset.id); } catch {} };
84
+ this._onDragOver = (e) => { if (this._dragId != null && e.target.closest('.nk-board-col')) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } };
85
+ this._onDrop = (e) => {
86
+ const col = e.target.closest('.nk-board-col');
87
+ if (!col || this._dragId == null) return;
88
+ e.preventDefault();
89
+ this.move(this._dragId, col.dataset.value);
90
+ this._dragId = null;
91
+ };
92
+ this._board.addEventListener('click', this._onClick);
93
+ this._board.addEventListener('dragstart', this._onDragStart);
94
+ this._board.addEventListener('dragover', this._onDragOver);
95
+ this._board.addEventListener('drop', this._onDrop);
96
+ }
97
+
98
+ teardownEvents() {
99
+ this._board?.removeEventListener('click', this._onClick);
100
+ this._board?.removeEventListener('dragstart', this._onDragStart);
101
+ this._board?.removeEventListener('dragover', this._onDragOver);
102
+ this._board?.removeEventListener('drop', this._onDrop);
103
+ }
104
+
105
+ /** Moves a row into another column (sets the group value) and reports it. */
106
+ move(id, value) {
107
+ const row = this._rowById(id), group = this._groupColumn();
108
+ if (!row || !group || row[group.key] === value) return;
109
+ row[group.key] = value;
110
+ this._render();
111
+ this.emit('nk-change', { row, key: group.key, value });
112
+ }
113
+
114
+ _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
115
+ onAttributeChanged() { this._render(); }
116
+
117
+ get name() { return this.getAttribute('name') || 'board'; }
118
+ get columns() { return this._columns; }
119
+ set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
120
+ get rows() { return this._rows; }
121
+ set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
122
+ set data({ columns, rows }) { this.setData(columns, rows); }
123
+ setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
124
+ refresh() { this._render(); }
125
+ }
126
+
127
+ customElements.define('nk-board-view', NkBoardView);
128
+ export { NkBoardView };
@@ -0,0 +1,40 @@
1
+ import { NkElement } from '../../base.js';
2
+
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>
5
+ // slot="head" adds content after the name (e.g. an <nk-tag>).
6
+ class NkComment extends NkElement {
7
+ static get observedAttributes() { return ['author', 'time', 'avatar', 'color']; }
8
+
9
+ render() {
10
+ const box = this.createElement('div', ['nk-comment']);
11
+ this._avatar = this.createElement('span', ['mini-avatar']);
12
+ const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
13
+ avatarSlot.appendChild(this._avatar);
14
+ const text = document.createElement('div');
15
+ this._head = this.createElement('div', ['c-head']);
16
+ this._author = document.createElement('b');
17
+ this._time = document.createTextNode('');
18
+ this._head.append(this._author, this.createElement('slot', [], { name: 'head' }), this._time);
19
+ const body = this.createElement('div', ['c-body']);
20
+ body.appendChild(document.createElement('slot'));
21
+ text.append(this._head, body);
22
+ box.append(avatarSlot, text);
23
+ this._wrapper.appendChild(box);
24
+ this._sync();
25
+ }
26
+
27
+ _sync() {
28
+ const author = this.getAttribute('author') || '';
29
+ this._author.textContent = author;
30
+ const time = this.getAttribute('time');
31
+ 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
+ }
35
+
36
+ onAttributeChanged() { this._sync(); }
37
+ }
38
+
39
+ customElements.define('nk-comment', NkComment);
40
+ export { NkComment };
@@ -0,0 +1,56 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-comments placeholder="Comment …" send-label="Send">
4
+ // <nk-comment …>…</nk-comment>
5
+ // </nk-comments>
6
+ // → <div class="nk-comments">…<div class="nk-comment-input"><input class="nk-input"><button class="nk-btn primary small">Send</button></div></div>
7
+ // Enter or the button fires nk-submit { text } and clears the field.
8
+ class NkComments extends NkElement {
9
+ static get observedAttributes() { return ['placeholder', 'send-label', 'no-input', 'disabled']; }
10
+
11
+ render() {
12
+ const box = this.createElement('div', ['nk-comments']);
13
+ box.appendChild(document.createElement('slot'));
14
+ this._row = this.createElement('div', ['nk-comment-input']);
15
+ this._input = this.createElement('input', ['nk-input'], { type: 'text', 'aria-label': 'Comment' });
16
+ this._btn = this.createElement('button', ['nk-btn', 'primary', 'small'], { type: 'button' });
17
+ this._row.append(this._input, this._btn);
18
+ box.appendChild(this._row);
19
+ this._wrapper.appendChild(box);
20
+ this._sync();
21
+ }
22
+
23
+ _sync() {
24
+ this._input.placeholder = this.getAttribute('placeholder') || 'Comment …';
25
+ this._btn.textContent = this.getAttribute('send-label') || 'Send';
26
+ this._row.style.display = this.getBoolAttr('no-input') ? 'none' : '';
27
+ this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
28
+ }
29
+
30
+ setupEvents() {
31
+ this._onSend = () => this.submit();
32
+ this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this.submit(); } };
33
+ this._btn.addEventListener('click', this._onSend);
34
+ this._input.addEventListener('keydown', this._onKey);
35
+ }
36
+
37
+ teardownEvents() {
38
+ this._btn?.removeEventListener('click', this._onSend);
39
+ this._input?.removeEventListener('keydown', this._onKey);
40
+ }
41
+
42
+ submit() {
43
+ const text = this._input.value.trim();
44
+ if (!text) return;
45
+ const ok = this.emit('nk-submit', { text });
46
+ if (ok) this._input.value = '';
47
+ }
48
+
49
+ onAttributeChanged() { this._sync(); }
50
+ focus(o) { this._input?.focus(o); }
51
+ get value() { return this._input?.value ?? ''; }
52
+ set value(v) { if (this._input) this._input.value = v ?? ''; }
53
+ }
54
+
55
+ customElements.define('nk-comments', NkComments);
56
+ export { NkComments };
@@ -0,0 +1,99 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-database id="db" view="table" add-view>
4
+ // <nk-table-view name="table" label="▦ Table" count></nk-table-view>
5
+ // <nk-board-view name="board" label="▤ Board" group-by="status"></nk-board-view>
6
+ // </nk-database>
7
+ // db.columns = [{ key: 'name', label: 'Name', type: 'text', icon: '📄', title: true }, { key: 'status', type: 'select', options: [...] }, …];
8
+ // db.rows = [{ id: 1, icon: '🧭', name: '…', status: 'done', … }];
9
+ // → <div class="nk-database"><div class="nk-db-tabs">…</div>…active view…</div>
10
+ // The tabs come from the child views; data is pushed into every view.
11
+ class NkDatabase extends NkElement {
12
+ static get observedAttributes() { return ['view', 'add-view']; }
13
+
14
+ render() {
15
+ this._box = this.createElement('div', ['nk-database']);
16
+ this._tabs = this.createElement('div', ['nk-db-tabs'], { role: 'tablist' });
17
+ this._slot = document.createElement('slot');
18
+ this._box.append(this._tabs, this._slot);
19
+ this._wrapper.appendChild(this._box);
20
+ this._columns = []; this._rows = [];
21
+ this._sync();
22
+ }
23
+
24
+ get views() { return this._slot?.assignedElements().filter(el => /^nk-[a-z]+-view$/.test(el.localName)) ?? []; }
25
+
26
+ _sync() {
27
+ if (!this._tabs) return;
28
+ const views = this.views;
29
+ let current = this.getAttribute('view');
30
+ if (!views.some(v => v.name === current)) current = views[0]?.name ?? null;
31
+ this._tabs.replaceChildren();
32
+ for (const v of views) {
33
+ const tab = this.createElement('span', ['nk-db-tab'], { role: 'tab', 'data-view': v.name, tabindex: '0' });
34
+ tab.classList.toggle('active', v.name === current);
35
+ tab.setAttribute('aria-selected', v.name === current ? 'true' : 'false');
36
+ tab.append(document.createTextNode(v.getAttribute('label') || v.name));
37
+ const badge = v.hasAttribute('count') ? String(this._rows.length) : v.getAttribute('badge');
38
+ if (badge) { const b = this.createElement('span', ['badge']); b.textContent = badge; tab.appendChild(b); }
39
+ this._tabs.appendChild(tab);
40
+ v.hidden = v.name !== current;
41
+ // A method call, not a property write: a view that has not been upgraded
42
+ // yet would otherwise get an own `data` property that shadows its setter.
43
+ v.setData?.(this._columns, this._rows);
44
+ }
45
+ if (this.getBoolAttr('add-view')) {
46
+ const add = this.createElement('span', ['nk-db-tab'], { role: 'button', 'data-add': '', tabindex: '0' });
47
+ add.style.color = 'var(--nk-text-tertiary)';
48
+ add.textContent = '+';
49
+ this._tabs.appendChild(add);
50
+ }
51
+ this._tabs.style.display = views.length ? '' : 'none';
52
+ this._current = current;
53
+ }
54
+
55
+ /** Views upgrade after the database; each one asks for a resync once rendered. */
56
+ requestSync() {
57
+ if (this._syncQueued) return;
58
+ this._syncQueued = true;
59
+ queueMicrotask(() => { this._syncQueued = false; this._sync(); });
60
+ }
61
+
62
+ setupEvents() {
63
+ this._onClick = (e) => {
64
+ const tab = e.target.closest('[data-view]');
65
+ if (tab) { this.view = tab.dataset.view; return; }
66
+ if (e.target.closest('[data-add]')) this.emit('nk-action', { action: 'add-view' });
67
+ };
68
+ this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.closest('[data-view],[data-add]')) { e.preventDefault(); e.target.click(); } };
69
+ this._onSlot = () => this._sync();
70
+ this._tabs.addEventListener('click', this._onClick);
71
+ this._tabs.addEventListener('keydown', this._onKey);
72
+ this._slot.addEventListener('slotchange', this._onSlot);
73
+ this._sync();
74
+ }
75
+
76
+ teardownEvents() {
77
+ this._tabs?.removeEventListener('click', this._onClick);
78
+ this._tabs?.removeEventListener('keydown', this._onKey);
79
+ this._slot?.removeEventListener('slotchange', this._onSlot);
80
+ }
81
+
82
+ onAttributeChanged(name) {
83
+ const before = this._current;
84
+ this._sync();
85
+ if (name === 'view' && this._current !== before) this.emit('nk-view-change', { view: this._current });
86
+ }
87
+
88
+ get columns() { return this._columns; }
89
+ set columns(v) { this._columns = Array.isArray(v) ? v : []; this._sync(); }
90
+ get rows() { return this._rows; }
91
+ set rows(v) { this._rows = Array.isArray(v) ? v : []; this._sync(); }
92
+ get view() { return this._current ?? this.getAttribute('view'); }
93
+ set view(v) { this.setAttribute('view', v); }
94
+ /** Re-renders every view after in-place row mutations. */
95
+ refresh() { this._sync(); }
96
+ }
97
+
98
+ customElements.define('nk-database', NkDatabase);
99
+ export { NkDatabase };
@@ -0,0 +1,77 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-filter-bar search placeholder="Search rows …"></nk-filter-bar>
4
+ // bar.filters = [{ key: 'status', label: 'Status: Done', value: 'done' }];
5
+ // Composed from existing classes (nk-btn secondary small, nk-input, nk-tag):
6
+ // filter / sort buttons fire nk-action, chips remove on ×, search and chip
7
+ // changes fire nk-change { filters, search }. The wrapper is layout only.
8
+ class NkFilterBar extends NkElement {
9
+ static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value']; }
10
+
11
+ render() {
12
+ this._bar = document.createElement('div');
13
+ this._bar.style.cssText = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0';
14
+ this._filterBtn = this.createElement('button', ['nk-btn', 'secondary', 'small'], { type: 'button', 'data-action': 'filter' });
15
+ this._filterBtn.textContent = '⚲ Filter';
16
+ this._sortBtn = this.createElement('button', ['nk-btn', 'secondary', 'small'], { type: 'button', 'data-action': 'sort' });
17
+ this._sortBtn.textContent = '↕ Sort';
18
+ this._chips = document.createElement('span');
19
+ this._chips.style.cssText = 'display:inline-flex;gap:4px;flex-wrap:wrap';
20
+ this._input = this.createElement('input', ['nk-input'], { type: 'search', autocomplete: 'off' });
21
+ this._input.style.marginLeft = 'auto';
22
+ this._bar.append(this._filterBtn, this._sortBtn, this._chips, document.createElement('slot'), this._input);
23
+ this._wrapper.appendChild(this._bar);
24
+ this._filters = [];
25
+ this._sync();
26
+ }
27
+
28
+ _sync() {
29
+ this._filterBtn.style.display = this.getBoolAttr('no-filter') ? 'none' : '';
30
+ this._sortBtn.style.display = this.getBoolAttr('no-sort') ? 'none' : '';
31
+ this._input.style.display = this.getBoolAttr('search') ? '' : 'none';
32
+ this._input.placeholder = this.getAttribute('placeholder') || 'Search …';
33
+ if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
34
+ this._chips.replaceChildren();
35
+ this._filters.forEach((f, i) => {
36
+ const chip = this.createElement('span', ['nk-tag', f.color || 'blue'], { 'data-index': i });
37
+ chip.style.cursor = 'pointer';
38
+ chip.textContent = `${f.label ?? `${f.key}: ${f.value}`} ×`;
39
+ chip.title = 'Remove filter';
40
+ this._chips.appendChild(chip);
41
+ });
42
+ }
43
+
44
+ setupEvents() {
45
+ this._onClick = (e) => {
46
+ const btn = e.target.closest('[data-action]');
47
+ if (btn) { this.emit('nk-action', { action: btn.dataset.action }); return; }
48
+ const chip = e.target.closest('[data-index]');
49
+ if (chip) { this._filters.splice(Number(chip.dataset.index), 1); this._sync(); this._emitChange(); }
50
+ };
51
+ this._onInput = () => { clearTimeout(this._debounce); this._debounce = setTimeout(() => this._emitChange(), 150); };
52
+ this._bar.addEventListener('click', this._onClick);
53
+ this._input.addEventListener('input', this._onInput);
54
+ }
55
+
56
+ teardownEvents() {
57
+ this._bar?.removeEventListener('click', this._onClick);
58
+ this._input?.removeEventListener('input', this._onInput);
59
+ clearTimeout(this._debounce);
60
+ }
61
+
62
+ _emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
63
+ onAttributeChanged() { this._sync(); }
64
+
65
+ get filters() { return this._filters; }
66
+ set filters(v) { this._filters = Array.isArray(v) ? v : []; this._sync(); }
67
+ get value() { return this._input?.value ?? ''; }
68
+ set value(v) { if (this._input) this._input.value = v ?? ''; }
69
+ /** Applies filters + search to rows: equality per filter key, substring search over every string field. */
70
+ apply(rows) {
71
+ const q = this.value.trim().toLowerCase();
72
+ return rows.filter(r => this._filters.every(f => r[f.key] === f.value) && (!q || Object.values(r).some(v => String(typeof v === 'object' && v ? v.name ?? '' : v ?? '').toLowerCase().includes(q))));
73
+ }
74
+ }
75
+
76
+ customElements.define('nk-filter-bar', NkFilterBar);
77
+ export { NkFilterBar };
@@ -0,0 +1,108 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { renderPropertyCell, compareBy } from '../../util/property-cell.js';
3
+
4
+ // <nk-table-view name="table" label="▦ Table" count new-row sortable></nk-table-view>
5
+ // Standalone: view.columns = […]; view.rows = […]; Inside <nk-database> the
6
+ // database pushes `data`. → <div class="nk-table-wrap"><table class="nk-table">…</table><div class="nk-new-row">+ New page</div></div>
7
+ // Cells are plain markup from renderPropertyCell(); rows fire nk-select,
8
+ // checkboxes nk-change, headers nk-action { action: 'sort' } (and sort locally
9
+ // with `sortable`), the add row nk-action { action: 'new-row' }.
10
+ class NkTableView extends NkElement {
11
+ static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir']; }
12
+
13
+ render() {
14
+ this._wrap = this.createElement('div', ['nk-table-wrap']);
15
+ this._table = this.createElement('table', ['nk-table']);
16
+ this._thead = document.createElement('thead');
17
+ this._tbody = document.createElement('tbody');
18
+ this._table.append(this._thead, this._tbody);
19
+ this._newRow = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0' });
20
+ this._wrap.append(this._table, this._newRow);
21
+ this._wrapper.appendChild(this._wrap);
22
+ this._columns ??= []; this._rows ??= [];
23
+ this._render();
24
+ this.closest('nk-database')?.requestSync?.();
25
+ }
26
+
27
+ _sortedRows() {
28
+ const key = this.getAttribute('sort-key');
29
+ const col = this._columns.find(c => c.key === key);
30
+ if (!col) return this._rows;
31
+ return [...this._rows].sort(compareBy(col, this.getAttribute('sort-dir') === 'desc' ? -1 : 1));
32
+ }
33
+
34
+ _render() {
35
+ if (!this._thead) return;
36
+ const tr = document.createElement('tr');
37
+ for (const col of this._columns) {
38
+ const th = this.createElement('th', [], { 'data-key': col.key, scope: 'col' });
39
+ if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
40
+ th.appendChild(document.createTextNode(col.label ?? col.key));
41
+ if (col.key === this.getAttribute('sort-key')) th.appendChild(document.createTextNode(this.getAttribute('sort-dir') === 'desc' ? ' ↓' : ' ↑'));
42
+ if (col.width) th.style.width = col.width;
43
+ tr.appendChild(th);
44
+ }
45
+ this._thead.replaceChildren(tr);
46
+ this._tbody.replaceChildren();
47
+ for (const row of this._sortedRows()) {
48
+ const r = this.createElement('tr', [], { 'data-id': row.id ?? '' });
49
+ for (const col of this._columns) {
50
+ const td = document.createElement('td');
51
+ td.appendChild(renderPropertyCell(col, row[col.key], row));
52
+ r.appendChild(td);
53
+ }
54
+ this._tbody.appendChild(r);
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 th = e.target.closest('th[data-key]');
64
+ if (th) {
65
+ const key = th.dataset.key;
66
+ const dir = this.getAttribute('sort-key') === key && this.getAttribute('sort-dir') !== 'desc' ? 'desc' : 'asc';
67
+ const ok = this.emit('nk-action', { action: 'sort', key, value: dir });
68
+ if (ok && this.getBoolAttr('sortable')) { this.setAttribute('sort-key', key); this.setAttribute('sort-dir', dir); }
69
+ return;
70
+ }
71
+ if (e.target.closest('input[type=checkbox], a')) return;
72
+ const tr = e.target.closest('tr[data-id]');
73
+ if (tr) { const row = this._rowById(tr.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
74
+ };
75
+ this._onChange = (e) => {
76
+ const input = e.target;
77
+ if (input.type !== 'checkbox') return;
78
+ const row = this._rowById(input.dataset.rowId);
79
+ if (row) row[input.dataset.key] = input.checked;
80
+ this.emit('nk-change', { row, key: input.dataset.key, value: input.checked });
81
+ };
82
+ this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target === this._newRow) { e.preventDefault(); this.emit('nk-action', { action: 'new-row' }); } };
83
+ this._wrap.addEventListener('click', this._onClick);
84
+ this._wrap.addEventListener('change', this._onChange);
85
+ this._wrap.addEventListener('keydown', this._onKey);
86
+ }
87
+
88
+ teardownEvents() {
89
+ this._wrap?.removeEventListener('click', this._onClick);
90
+ this._wrap?.removeEventListener('change', this._onChange);
91
+ this._wrap?.removeEventListener('keydown', this._onKey);
92
+ }
93
+
94
+ _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
95
+ onAttributeChanged() { this._render(); }
96
+
97
+ get name() { return this.getAttribute('name') || 'table'; }
98
+ get columns() { return this._columns; }
99
+ set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
100
+ get rows() { return this._rows; }
101
+ set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
102
+ set data({ columns, rows }) { this.setData(columns, rows); }
103
+ setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
104
+ refresh() { this._render(); }
105
+ }
106
+
107
+ customElements.define('nk-table-view', NkTableView);
108
+ export { NkTableView };