@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,84 @@
1
+ import { NkFormElement } from '../../base.js';
2
+
3
+ // <nk-slider name="size" min="12" max="18" value="14" show-value></nk-slider>
4
+ // → <input type="range" class="nk-slider"> <div class="nk-slider-value">14</div>
5
+ const FORWARDED = ['name', 'min', 'max', 'step', 'aria-label'];
6
+
7
+ class NkSlider extends NkFormElement {
8
+ static get observedAttributes() { return ['value', 'disabled', 'show-value', 'unit', ...FORWARDED]; }
9
+
10
+ render() {
11
+ this._input = this.createElement('input', ['nk-slider'], { type: 'range' });
12
+ this._out = this.createElement('div', ['nk-slider-value']);
13
+ this._apply();
14
+ this._input.value = this.getAttribute('value') ?? this._input.value;
15
+ this._defaultValue = this._input.value;
16
+ this._wrapper.appendChild(this._input);
17
+ this._wrapper.appendChild(this._out);
18
+ this._syncOut();
19
+ this._syncFormValue();
20
+ }
21
+
22
+ _apply() {
23
+ for (const attr of FORWARDED) {
24
+ const v = this.getAttribute(attr);
25
+ v === null ? this._input.removeAttribute(attr) : this._input.setAttribute(attr, v);
26
+ }
27
+ this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
28
+ this._out.style.display = this.getBoolAttr('show-value') ? '' : 'none';
29
+ }
30
+
31
+ _syncOut() {
32
+ this._out.textContent = `${this._input.value}${this.getAttribute('unit') || ''}`;
33
+ }
34
+
35
+ setupEvents() {
36
+ this._onInput = () => {
37
+ this._syncOut();
38
+ this._syncFormValue();
39
+ this.emit('nk-input', { value: Number(this._input.value), name: this.name });
40
+ };
41
+ this._onChange = () => {
42
+ this._syncFormValue();
43
+ this.emit('nk-change', { value: Number(this._input.value), name: this.name });
44
+ this.dispatchEvent(new Event('change', { bubbles: true }));
45
+ };
46
+ this._input.addEventListener('input', this._onInput);
47
+ this._input.addEventListener('change', this._onChange);
48
+ }
49
+
50
+ teardownEvents() {
51
+ this._input?.removeEventListener('input', this._onInput);
52
+ this._input?.removeEventListener('change', this._onChange);
53
+ }
54
+
55
+ onAttributeChanged(name) {
56
+ if (!this._input) return;
57
+ if (name === 'value') this._input.value = this.getAttribute('value') ?? '';
58
+ else this._apply();
59
+ this._syncOut();
60
+ this._syncFormValue();
61
+ }
62
+
63
+ _syncFormValue() { this.setFormValue(this._input.value); }
64
+ resetValue() { this._input.value = this._defaultValue; this._syncOut(); this._syncFormValue(); }
65
+ restoreValue(state) { if (typeof state === 'string') { this._input.value = state; this._syncOut(); this._syncFormValue(); } }
66
+ onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
67
+
68
+ focus(options) { this._input?.focus(options); }
69
+
70
+ get value() { return Number(this._input?.value ?? this.getAttribute('value')); }
71
+ set value(v) {
72
+ if (this._input) { this._input.value = v; this._syncOut(); this._syncFormValue(); }
73
+ else this.setAttribute('value', v);
74
+ }
75
+ get name() { return this.getAttribute('name'); }
76
+ set name(v) { this.setAttribute('name', v); }
77
+ get disabled() { return this.getBoolAttr('disabled'); }
78
+ set disabled(v) { this.setBoolAttr('disabled', v); }
79
+ get showValue() { return this.getBoolAttr('show-value'); }
80
+ set showValue(v) { this.setBoolAttr('show-value', v); }
81
+ }
82
+
83
+ customElements.define('nk-slider', NkSlider);
84
+ export { NkSlider };
@@ -0,0 +1,68 @@
1
+ import { NkFormElement } from '../../base.js';
2
+
3
+ // <nk-switch name="notify" checked></nk-switch>
4
+ // → <button class="nk-switch" role="switch" aria-checked="true">
5
+ // notionkit.css styles the state through [aria-checked]; the toggling is here.
6
+ class NkSwitch extends NkFormElement {
7
+ static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'label']; }
8
+
9
+ render() {
10
+ this._btn = this.createElement('button', ['nk-switch'], { type: 'button', role: 'switch' });
11
+ this._defaultChecked = this.getBoolAttr('checked');
12
+ this._apply();
13
+ this._wrapper.appendChild(this._btn);
14
+ this._syncFormValue();
15
+ }
16
+
17
+ _apply() {
18
+ this._btn.setAttribute('aria-checked', this.getBoolAttr('checked') ? 'true' : 'false');
19
+ this._btn.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
20
+ const label = this.getAttribute('label');
21
+ label ? this._btn.setAttribute('aria-label', label) : this._btn.removeAttribute('aria-label');
22
+ }
23
+
24
+ setupEvents() {
25
+ this._onClick = () => this.toggle();
26
+ this._btn.addEventListener('click', this._onClick);
27
+ }
28
+
29
+ teardownEvents() {
30
+ this._btn?.removeEventListener('click', this._onClick);
31
+ }
32
+
33
+ toggle() {
34
+ if (this.getBoolAttr('disabled') || this._formDisabled) return;
35
+ this.checked = !this.checked;
36
+ this.emit('nk-change', { checked: this.checked, value: this.getAttribute('value') || 'on', name: this.name });
37
+ this.dispatchEvent(new Event('change', { bubbles: true }));
38
+ }
39
+
40
+ onAttributeChanged() {
41
+ if (!this._btn) return;
42
+ this._apply();
43
+ this._syncFormValue();
44
+ }
45
+
46
+ _syncFormValue() {
47
+ const val = this.getAttribute('value') || 'on';
48
+ this.setFormValue(this.getBoolAttr('checked') ? val : null, this.getBoolAttr('checked') ? 'checked' : 'unchecked');
49
+ }
50
+
51
+ resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
52
+ restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
53
+ onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
54
+
55
+ focus(options) { this._btn?.focus(options); }
56
+
57
+ get checked() { return this.getBoolAttr('checked'); }
58
+ set checked(v) { this.setBoolAttr('checked', v); }
59
+ get disabled() { return this.getBoolAttr('disabled'); }
60
+ set disabled(v) { this.setBoolAttr('disabled', v); }
61
+ get name() { return this.getAttribute('name'); }
62
+ set name(v) { this.setAttribute('name', v); }
63
+ get value() { return this.getAttribute('value') || 'on'; }
64
+ set value(v) { this.setAttribute('value', v); }
65
+ }
66
+
67
+ customElements.define('nk-switch', NkSwitch);
68
+ export { NkSwitch };
@@ -0,0 +1,94 @@
1
+ import { NkFormElement } from '../../base.js';
2
+
3
+ // <nk-textarea name="bio" rows="3" wide>Text</nk-textarea> → <textarea class="nk-textarea wide">
4
+ // The initial value is the `value` attribute, or the element's text content.
5
+ const FORWARDED = ['placeholder', 'name', 'rows', 'maxlength', 'minlength', 'autocomplete', 'aria-label'];
6
+ const BOOLEANS = ['required', 'disabled', 'readonly'];
7
+
8
+ class NkTextarea extends NkFormElement {
9
+ static get observedAttributes() { return ['value', 'wide', ...FORWARDED, ...BOOLEANS]; }
10
+
11
+ render() {
12
+ this._input = this.createElement('textarea', ['nk-textarea']);
13
+ for (const attr of [...FORWARDED, ...BOOLEANS]) this._forward(attr);
14
+ this._input.value = this.getAttribute('value') ?? this.textContent.trim();
15
+ this._syncWide();
16
+ this._wrapper.appendChild(this._input);
17
+ this._syncFormValue();
18
+ }
19
+
20
+ _forward(attr) {
21
+ if (BOOLEANS.includes(attr)) {
22
+ this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
23
+ return;
24
+ }
25
+ const v = this.getAttribute(attr);
26
+ if (v === null) this._input.removeAttribute(attr);
27
+ else this._input.setAttribute(attr, v);
28
+ }
29
+
30
+ _syncWide() { this._input.classList.toggle('wide', this.getBoolAttr('wide')); }
31
+
32
+ setupEvents() {
33
+ this._onInput = () => {
34
+ this._syncFormValue();
35
+ this.emit('nk-input', { value: this._input.value, name: this.name });
36
+ };
37
+ this._onChange = () => {
38
+ this._syncFormValue();
39
+ this.emit('nk-change', { value: this._input.value, name: this.name });
40
+ this.dispatchEvent(new Event('change', { bubbles: true }));
41
+ };
42
+ this._input.addEventListener('input', this._onInput);
43
+ this._input.addEventListener('change', this._onChange);
44
+ }
45
+
46
+ teardownEvents() {
47
+ this._input?.removeEventListener('input', this._onInput);
48
+ this._input?.removeEventListener('change', this._onChange);
49
+ }
50
+
51
+ onAttributeChanged(name) {
52
+ if (!this._input) return;
53
+ if (name === 'value') { this._input.value = this.getAttribute('value') ?? ''; this._syncFormValue(); return; }
54
+ if (name === 'wide') { this._syncWide(); return; }
55
+ this._forward(name);
56
+ this._syncFormValue();
57
+ }
58
+
59
+ _syncFormValue() {
60
+ this.setFormValue(this._input.value);
61
+ this.syncValidityFrom(this._input);
62
+ }
63
+
64
+ resetValue() {
65
+ this._input.value = this.getAttribute('value') ?? this.textContent.trim();
66
+ this._syncFormValue();
67
+ }
68
+
69
+ restoreValue(state) {
70
+ if (typeof state === 'string') { this._input.value = state; this._syncFormValue(); }
71
+ }
72
+
73
+ onFormDisabled(disabled) { this._input.disabled = disabled || this.getBoolAttr('disabled'); }
74
+
75
+ focus(options) { this._input?.focus(options); }
76
+ blur() { this._input?.blur(); }
77
+
78
+ get value() { return this._input?.value ?? (this.getAttribute('value') ?? ''); }
79
+ set value(v) {
80
+ if (this._input) { this._input.value = v ?? ''; this._syncFormValue(); }
81
+ else this.setAttribute('value', v ?? '');
82
+ }
83
+ get name() { return this.getAttribute('name'); }
84
+ set name(v) { this.setAttribute('name', v); }
85
+ get disabled() { return this.getBoolAttr('disabled'); }
86
+ set disabled(v) { this.setBoolAttr('disabled', v); }
87
+ get required() { return this.getBoolAttr('required'); }
88
+ set required(v) { this.setBoolAttr('required', v); }
89
+ get wide() { return this.getBoolAttr('wide'); }
90
+ set wide(v) { this.setBoolAttr('wide', v); }
91
+ }
92
+
93
+ customElements.define('nk-textarea', NkTextarea);
94
+ export { NkTextarea };
@@ -0,0 +1,201 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { lockScroll, unlockScroll, inertOutside } from '../../util/focus.js';
3
+ import { fuzzyScore } from '../../util/fuzzy.js';
4
+
5
+ // <nk-cmdk id="palette" placeholder="Search or type a command …"></nk-cmdk>
6
+ // palette.commands = [
7
+ // { group: 'Pages', items: [{ id: 'mvp', icon: '🚀', label: 'NotionKit MVP', keywords: 'project' }] },
8
+ // { group: 'Actions', items: [{ id: 'theme', icon: '🌙', label: 'Toggle theme', shortcut: '⌘⇧L', action: () => … }] },
9
+ // ];
10
+ // → <div class="nk-cmdk-backdrop open"><div class="nk-cmdk"><div class="nk-cmdk-input-row">🔍<input><kbd>esc</kbd></div>
11
+ // <div class="nk-cmdk-list">…</div><div class="nk-cmdk-footer">…</div></div></div>
12
+ // mod+K toggles (attribute `hotkey`), ↑↓ move, Enter picks (nk-command +
13
+ // item.action), Escape / backdrop close. Fuzzy subsequence search over
14
+ // label + keywords. Put the element directly under <body>.
15
+ class NkCmdk extends NkElement {
16
+ static get observedAttributes() { return ['open', 'hotkey', 'placeholder']; }
17
+
18
+ render() {
19
+ this._backdrop = this.createElement('div', ['nk-cmdk-backdrop']);
20
+ this._backdrop.inert = true;
21
+ this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette' });
22
+ const row = this.createElement('div', ['nk-cmdk-input-row']);
23
+ const glass = document.createElement('span');
24
+ glass.style.fontSize = '15px';
25
+ glass.textContent = '🔍';
26
+ this._input = this.createElement('input', [], { type: 'text', autocomplete: 'off', role: 'combobox', 'aria-expanded': 'true', 'aria-autocomplete': 'list' });
27
+ const esc = this.createElement('kbd', ['nk-kbd']);
28
+ esc.textContent = 'esc';
29
+ row.append(glass, this._input, esc);
30
+ this._list = this.createElement('div', ['nk-cmdk-list'], { role: 'listbox', id: 'list' });
31
+ this._input.setAttribute('aria-controls', 'list');
32
+ this._footer = this.createElement('div', ['nk-cmdk-footer']);
33
+ const footerSlot = this.createElement('slot', [], { name: 'footer' });
34
+ footerSlot.innerHTML = '<span><kbd class="nk-kbd">↑</kbd><kbd class="nk-kbd">↓</kbd> navigate</span><span><kbd class="nk-kbd">↵</kbd> open</span><span><kbd class="nk-kbd">⌘</kbd><kbd class="nk-kbd">K</kbd> toggle</span>';
35
+ this._footer.appendChild(footerSlot);
36
+ this._box.append(row, this._list, this._footer);
37
+ this._backdrop.appendChild(this._box);
38
+ this._wrapper.appendChild(this._backdrop);
39
+ this._commands = [];
40
+ this._index = 0;
41
+ this._syncAttrs();
42
+ this._renderList();
43
+ this._syncOpen();
44
+ }
45
+
46
+ _syncAttrs() { this._input.placeholder = this.getAttribute('placeholder') || 'Search or type a command …'; }
47
+
48
+ /** Filtered, flat list in group order, best matches first within a group. */
49
+ results() {
50
+ const q = this._input?.value ?? '';
51
+ const out = [];
52
+ for (const group of this._commands) {
53
+ const scored = (group.items || []).map(item => ({ item, group: group.group, score: fuzzyScore(q, `${item.label} ${item.keywords || ''}`) })).filter(r => r.score > 0);
54
+ scored.sort((a, b) => b.score - a.score);
55
+ out.push(...scored);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ _renderList() {
61
+ if (!this._list) return;
62
+ const results = this.results();
63
+ this._index = Math.min(this._index, Math.max(0, results.length - 1));
64
+ this._list.replaceChildren();
65
+ if (!results.length) {
66
+ const empty = this.createElement('div', ['nk-cmdk-empty']);
67
+ empty.textContent = `No results for “${this._input.value}”`;
68
+ this._list.appendChild(empty);
69
+ this._input.removeAttribute('aria-activedescendant');
70
+ return;
71
+ }
72
+ let lastGroup = null;
73
+ results.forEach((r, i) => {
74
+ if (r.group && r.group !== lastGroup) {
75
+ const g = this.createElement('div', ['nk-cmdk-group']);
76
+ g.textContent = r.group;
77
+ this._list.appendChild(g);
78
+ lastGroup = r.group;
79
+ }
80
+ const el = this.createElement('div', ['nk-cmdk-item'], { role: 'option', id: `opt-${i}`, 'data-index': i });
81
+ el.classList.toggle('selected', i === this._index);
82
+ el.setAttribute('aria-selected', i === this._index ? 'true' : 'false');
83
+ const icon = this.createElement('span', ['m-icon']);
84
+ icon.textContent = r.item.icon || '';
85
+ const label = document.createElement('span');
86
+ label.textContent = r.item.label;
87
+ el.append(icon, label);
88
+ if (r.item.shortcut) { const sc = this.createElement('span', ['m-shortcut']); sc.textContent = r.item.shortcut; el.appendChild(sc); }
89
+ this._list.appendChild(el);
90
+ });
91
+ this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
92
+ }
93
+
94
+ _move(dir) {
95
+ const n = this.results().length;
96
+ if (!n) return;
97
+ this._index = (this._index + dir + n) % n;
98
+ for (const el of this._list.querySelectorAll('.nk-cmdk-item')) {
99
+ const on = Number(el.dataset.index) === this._index;
100
+ el.classList.toggle('selected', on);
101
+ el.setAttribute('aria-selected', on ? 'true' : 'false');
102
+ if (on) el.scrollIntoView({ block: 'nearest' });
103
+ }
104
+ this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
105
+ }
106
+
107
+ pick(index = this._index) {
108
+ const r = this.results()[index];
109
+ if (!r) return;
110
+ const query = this._input.value;
111
+ this.close();
112
+ const ok = this.emit('nk-command', { id: r.item.id ?? r.item.label, item: r.item, query });
113
+ if (ok && typeof r.item.action === 'function') r.item.action(r.item);
114
+ }
115
+
116
+ _matchesHotkey(e) {
117
+ const spec = (this.getAttribute('hotkey') || 'mod+k').toLowerCase().split('+');
118
+ const key = spec.pop();
119
+ const mod = spec.includes('mod') ? (e.metaKey || e.ctrlKey) : true;
120
+ const shift = spec.includes('shift') ? e.shiftKey : !e.shiftKey;
121
+ const alt = spec.includes('alt') ? e.altKey : !e.altKey;
122
+ return mod && shift && alt && e.key.toLowerCase() === key;
123
+ }
124
+
125
+ _syncOpen() {
126
+ const open = this.getBoolAttr('open');
127
+ this._backdrop.classList.toggle('open', open);
128
+ this._backdrop.inert = !open;
129
+ if (open === this._wasOpen) return;
130
+ this._wasOpen = open;
131
+ if (open) {
132
+ this._returnFocus = document.activeElement;
133
+ this._input.value = '';
134
+ this._index = 0;
135
+ this._renderList();
136
+ lockScroll();
137
+ this._undoInert = inertOutside(this);
138
+ requestAnimationFrame(() => this._input.focus({ preventScroll: true }));
139
+ } else {
140
+ unlockScroll();
141
+ this._undoInert?.(); this._undoInert = null;
142
+ const back = this._returnFocus;
143
+ if (back && typeof back.focus === 'function' && back.isConnected) back.focus({ preventScroll: true });
144
+ this._returnFocus = null;
145
+ }
146
+ }
147
+
148
+ setupEvents() {
149
+ this._onDocKey = (e) => {
150
+ if (this._matchesHotkey(e)) { e.preventDefault(); this.toggle(); return; }
151
+ if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); }
152
+ };
153
+ this._onInput = () => { this._index = 0; this._renderList(); };
154
+ this._onInputKey = (e) => {
155
+ if (e.key === 'ArrowDown') { e.preventDefault(); this._move(1); }
156
+ else if (e.key === 'ArrowUp') { e.preventDefault(); this._move(-1); }
157
+ else if (e.key === 'Enter') { e.preventDefault(); this.pick(); }
158
+ };
159
+ this._onListClick = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el) this.pick(Number(el.dataset.index)); };
160
+ this._onListMove = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el && Number(el.dataset.index) !== this._index) { this._index = Number(el.dataset.index); this._move(0); } };
161
+ this._onListDown = (e) => e.preventDefault(); // keep the input focused
162
+ this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
163
+ document.addEventListener('keydown', this._onDocKey);
164
+ this._input.addEventListener('input', this._onInput);
165
+ this._input.addEventListener('keydown', this._onInputKey);
166
+ this._list.addEventListener('click', this._onListClick);
167
+ this._list.addEventListener('mousemove', this._onListMove);
168
+ this._list.addEventListener('mousedown', this._onListDown);
169
+ this._backdrop.addEventListener('click', this._onBackdrop);
170
+ }
171
+
172
+ teardownEvents() {
173
+ document.removeEventListener('keydown', this._onDocKey);
174
+ this._input?.removeEventListener('input', this._onInput);
175
+ this._input?.removeEventListener('keydown', this._onInputKey);
176
+ this._list?.removeEventListener('click', this._onListClick);
177
+ this._list?.removeEventListener('mousemove', this._onListMove);
178
+ this._list?.removeEventListener('mousedown', this._onListDown);
179
+ this._backdrop?.removeEventListener('click', this._onBackdrop);
180
+ if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
181
+ }
182
+
183
+ onAttributeChanged(name) {
184
+ if (name === 'open') { this._syncOpen(); this.emit('nk-toggle', { open: this.getBoolAttr('open') }); }
185
+ else this._syncAttrs();
186
+ }
187
+
188
+ show() { this.setBoolAttr('open', true); }
189
+ close() { this.setBoolAttr('open', false); }
190
+ toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
191
+
192
+ get commands() { return this._commands; }
193
+ set commands(list) { this._commands = Array.isArray(list) ? list : []; this._index = 0; this._renderList(); }
194
+ get open() { return this.getBoolAttr('open'); }
195
+ set open(v) { this.setBoolAttr('open', v); }
196
+ get query() { return this._input?.value ?? ''; }
197
+ set query(v) { if (this._input) { this._input.value = v; this._index = 0; this._renderList(); } }
198
+ }
199
+
200
+ customElements.define('nk-cmdk', NkCmdk);
201
+ export { NkCmdk };
@@ -0,0 +1,94 @@
1
+ import { NkElement } from '../../base.js';
2
+ import { EMOJI, EMOJI_CATEGORIES } from '../../util/emoji.js';
3
+
4
+ // <nk-emoji-picker></nk-emoji-picker>
5
+ // → <div class="nk-pop"><input class="nk-emoji-search"><div class="nk-emoji-grid">…</div><div class="nk-emoji-cats">…</div></div>
6
+ // Built-in set with names for search; `picker.emojis = [{ char, name, cat }]`
7
+ // replaces it. A click fires nk-select { emoji }.
8
+ class NkEmojiPicker extends NkElement {
9
+ static get observedAttributes() { return ['placeholder', 'value']; }
10
+
11
+ render() {
12
+ this._box = this.createElement('div', ['nk-pop']);
13
+ this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off', 'aria-label': 'Search emoji' });
14
+ this._grid = this.createElement('div', ['nk-emoji-grid'], { role: 'listbox' });
15
+ this._cats = this.createElement('div', ['nk-emoji-cats'], { role: 'tablist' });
16
+ this._box.append(this._search, this._grid, this._cats);
17
+ this._wrapper.appendChild(this._box);
18
+ this._emojis = EMOJI;
19
+ this._cat = EMOJI_CATEGORIES[0].id;
20
+ this._renderCats();
21
+ this._renderGrid();
22
+ this._syncAttrs();
23
+ }
24
+
25
+ _syncAttrs() { this._search.placeholder = this.getAttribute('placeholder') || 'Search…'; }
26
+
27
+ _renderCats() {
28
+ this._cats.replaceChildren();
29
+ const cats = this._categories();
30
+ for (const cat of cats) {
31
+ const el = this.createElement('span', [], { role: 'tab', 'data-cat': cat.id, title: cat.id });
32
+ el.textContent = cat.icon;
33
+ el.classList.toggle('active', cat.id === this._cat);
34
+ this._cats.appendChild(el);
35
+ }
36
+ this._cats.style.display = cats.length > 1 ? '' : 'none';
37
+ }
38
+
39
+ _categories() {
40
+ if (this._catList) return this._catList;
41
+ const seen = new Map();
42
+ for (const e of this._emojis) if (e.cat && !seen.has(e.cat)) seen.set(e.cat, { id: e.cat, icon: e.icon || EMOJI_CATEGORIES.find(c => c.id === e.cat)?.icon || e.char });
43
+ return [...seen.values()];
44
+ }
45
+
46
+ _renderGrid() {
47
+ const q = this._search.value.trim().toLowerCase();
48
+ const list = q ? this._emojis.filter(e => (e.name || '').toLowerCase().includes(q) || e.char === q) : this._emojis.filter(e => !e.cat || e.cat === this._cat);
49
+ this._grid.replaceChildren();
50
+ for (const e of list.slice(0, 64)) {
51
+ const s = this.createElement('span', [], { role: 'option', title: e.name || '', 'data-emoji': e.char });
52
+ s.textContent = e.char;
53
+ this._grid.appendChild(s);
54
+ }
55
+ for (const el of this._cats.children) el.classList.toggle('active', !q && el.dataset.cat === this._cat);
56
+ }
57
+
58
+ setupEvents() {
59
+ this._onInput = () => this._renderGrid();
60
+ this._onGrid = (e) => { const s = e.target.closest('[data-emoji]'); if (s) this.select(s.dataset.emoji); };
61
+ this._onCats = (e) => { const c = e.target.closest('[data-cat]'); if (!c) return; this._cat = c.dataset.cat; this._search.value = ''; this._renderGrid(); };
62
+ this._onKey = (e) => { if (e.key === 'Enter') { const first = this._grid.querySelector('[data-emoji]'); if (first) this.select(first.dataset.emoji); } };
63
+ this._search.addEventListener('input', this._onInput);
64
+ this._search.addEventListener('keydown', this._onKey);
65
+ this._grid.addEventListener('click', this._onGrid);
66
+ this._cats.addEventListener('click', this._onCats);
67
+ }
68
+
69
+ teardownEvents() {
70
+ this._search?.removeEventListener('input', this._onInput);
71
+ this._search?.removeEventListener('keydown', this._onKey);
72
+ this._grid?.removeEventListener('click', this._onGrid);
73
+ this._cats?.removeEventListener('click', this._onCats);
74
+ }
75
+
76
+ select(emoji) {
77
+ this.setAttribute('value', emoji);
78
+ this.emit('nk-select', { emoji, value: emoji });
79
+ }
80
+
81
+ onAttributeChanged() { this._syncAttrs(); }
82
+ focus(o) { this._search?.focus(o); }
83
+
84
+ /** Category strip override: [{ id, icon }]. Emojis without `cat` show in every category. */
85
+ get categories() { return this._categories(); }
86
+ set categories(list) { this._catList = Array.isArray(list) && list.length ? list : null; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
87
+ get emojis() { return this._emojis; }
88
+ set emojis(list) { this._emojis = Array.isArray(list) ? list : EMOJI; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
89
+ get value() { return this.getAttribute('value'); }
90
+ set value(v) { this.setAttribute('value', v); }
91
+ }
92
+
93
+ customElements.define('nk-emoji-picker', NkEmojiPicker);
94
+ export { NkEmojiPicker };
@@ -0,0 +1,67 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
4
+ // <nk-menu-item type="separator"></nk-menu-item> <nk-menu-item type="label">Danger</nk-menu-item>
5
+ // <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
6
+ // → <div class="nk-menu-item danger"><span class="m-icon">🗑️</span>Delete<span class="m-shortcut">…</span></div>
7
+ class NkMenuItem extends NkElement {
8
+ static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled']; }
9
+
10
+ render() { this._build(); }
11
+
12
+ _build() {
13
+ const type = this.getAttribute('type');
14
+ let el;
15
+ if (type === 'separator') {
16
+ el = this.createElement('div', ['nk-menu-sep'], { role: 'separator' });
17
+ } else if (type === 'label') {
18
+ el = this.createElement('div', ['nk-menu-label']);
19
+ el.appendChild(document.createElement('slot'));
20
+ } else {
21
+ el = this.createElement('div', ['nk-menu-item'], { role: 'menuitem', tabindex: '-1' });
22
+ this._icon = this.createElement('span', ['m-icon']);
23
+ const iconSlot = this.createElement('slot', [], { name: 'icon' });
24
+ iconSlot.appendChild(this._icon);
25
+ this._shortcut = this.createElement('span', ['m-shortcut']);
26
+ el.append(iconSlot, document.createElement('slot'), this._shortcut);
27
+ }
28
+ if (this._el) { this._el.removeEventListener('click', this._onClick); this._el.replaceWith(el); } else this._wrapper.appendChild(el);
29
+ this._el = el;
30
+ if (this._onClick) el.addEventListener('click', this._onClick);
31
+ this._sync();
32
+ }
33
+
34
+ _sync() {
35
+ if (!this._el.classList.contains('nk-menu-item')) return;
36
+ const icon = this.getAttribute('icon');
37
+ this._icon.textContent = icon || '';
38
+ this._icon.style.display = icon ? '' : 'none';
39
+ const sc = this.getAttribute('shortcut');
40
+ this._shortcut.textContent = sc || '';
41
+ this._shortcut.style.display = sc ? '' : 'none';
42
+ this._el.classList.toggle('danger', this.getBoolAttr('danger'));
43
+ this._el.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
44
+ }
45
+
46
+ setupEvents() {
47
+ this._onClick = () => this.select();
48
+ this._el.addEventListener('click', this._onClick);
49
+ }
50
+
51
+ teardownEvents() { this._el?.removeEventListener('click', this._onClick); }
52
+
53
+ select() {
54
+ if (!this._el.classList.contains('nk-menu-item') || this.getBoolAttr('disabled')) return;
55
+ this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
56
+ }
57
+
58
+ onAttributeChanged(name) { name === 'type' ? this._build() : this._sync(); }
59
+ focus(o) { this._el?.focus(o); }
60
+ get selectable() { return this._el?.classList.contains('nk-menu-item') && !this.getBoolAttr('disabled'); }
61
+
62
+ get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
63
+ set value(v) { this.setAttribute('value', v); }
64
+ }
65
+
66
+ customElements.define('nk-menu-item', NkMenuItem);
67
+ export { NkMenuItem };
@@ -0,0 +1,44 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-menu>
4
+ // <nk-menu-item type="label">Page</nk-menu-item>
5
+ // <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
6
+ // <nk-menu-item type="separator"></nk-menu-item>
7
+ // <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
8
+ // </nk-menu>
9
+ // → <div class="nk-pop nk-menu" role="menu">…</div>
10
+ // ↑↓ move between items, Enter selects, nk-select bubbles from the item.
11
+ class NkMenu extends NkElement {
12
+ render() {
13
+ this._box = this.createElement('div', ['nk-pop', 'nk-menu'], { role: 'menu' });
14
+ this._box.appendChild(document.createElement('slot'));
15
+ this._wrapper.appendChild(this._box);
16
+ }
17
+
18
+ get items() { return [...this.querySelectorAll(':scope > nk-menu-item')].filter(i => i.selectable); }
19
+
20
+ setupEvents() {
21
+ this._onKey = (e) => {
22
+ const dir = { ArrowDown: 1, ArrowUp: -1 }[e.key];
23
+ const current = e.target.closest?.('nk-menu-item');
24
+ const items = this.items;
25
+ if (!items.length) return;
26
+ if (dir) {
27
+ e.preventDefault();
28
+ const i = items.indexOf(current);
29
+ items[(i + dir + items.length) % items.length].focus();
30
+ } else if ((e.key === 'Enter' || e.key === ' ') && current) {
31
+ e.preventDefault(); current.select();
32
+ }
33
+ };
34
+ this.addEventListener('keydown', this._onKey);
35
+ }
36
+
37
+ teardownEvents() { this.removeEventListener('keydown', this._onKey); }
38
+
39
+ /** Focuses the first item (called by a popover when it opens). */
40
+ focusFirst() { this.items[0]?.focus(); }
41
+ }
42
+
43
+ customElements.define('nk-menu', NkMenu);
44
+ export { NkMenu };