@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,88 @@
1
+ import { NkFormElement } from '../../base.js';
2
+
3
+ // <nk-model-card name="model" value="gpt" title="Mona Pro" desc="Best for long documents" selected></nk-model-card>
4
+ // → <div class="nk-model-card selected"><div class="m-radio"></div><div><div class="m-name">…</div><div class="m-desc">…</div></div></div>
5
+ // Cards with the same `name` in the same tree behave like radios and submit
6
+ // the selected value with the form.
7
+ class NkModelCard extends NkFormElement {
8
+ static get observedAttributes() { return ['name', 'value', 'title', 'desc', 'selected', 'disabled']; }
9
+
10
+ render() {
11
+ this._card = this.createElement('div', ['nk-model-card'], { role: 'radio', tabindex: '0' });
12
+ this._card.appendChild(this.createElement('div', ['m-radio']));
13
+ const text = document.createElement('div');
14
+ this._name = this.createElement('div', ['m-name']);
15
+ const titleSlot = this.createElement('slot', [], { name: 'title' });
16
+ titleSlot.appendChild(this._name);
17
+ this._desc = this.createElement('div', ['m-desc']);
18
+ const descSlot = this.createElement('slot', [], { name: 'desc' });
19
+ descSlot.appendChild(this._desc);
20
+ text.append(titleSlot, descSlot);
21
+ this._card.appendChild(text);
22
+ this._wrapper.appendChild(this._card);
23
+ this._defaultSelected = this.getBoolAttr('selected');
24
+ this._sync();
25
+ if (this.getBoolAttr('selected')) this._unselectPeers();
26
+ }
27
+
28
+ _sync() {
29
+ const on = this.getBoolAttr('selected');
30
+ this._card.classList.toggle('selected', on);
31
+ this._card.setAttribute('aria-checked', on ? 'true' : 'false');
32
+ this._card.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
33
+ this._name.textContent = this.getAttribute('title') || '';
34
+ const desc = this.getAttribute('desc');
35
+ this._desc.textContent = desc || '';
36
+ this._desc.style.display = desc ? '' : 'none';
37
+ this.setFormValue(on ? (this.getAttribute('value') || 'on') : null, on ? 'selected' : 'unselected');
38
+ }
39
+
40
+ _group() {
41
+ const name = this.getAttribute('name');
42
+ const root = this.getRootNode();
43
+ if (!name || typeof root?.querySelectorAll !== 'function') return [this];
44
+ return [...root.querySelectorAll('nk-model-card')].filter(el => el.getAttribute('name') === name);
45
+ }
46
+
47
+ _unselectPeers() {
48
+ for (const el of this._group()) if (el !== this && el.hasAttribute('selected')) el.removeAttribute('selected');
49
+ }
50
+
51
+ setupEvents() {
52
+ this._onClick = () => this.select();
53
+ this._onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); } };
54
+ this._card.addEventListener('click', this._onClick);
55
+ this._card.addEventListener('keydown', this._onKey);
56
+ }
57
+
58
+ teardownEvents() {
59
+ this._card?.removeEventListener('click', this._onClick);
60
+ this._card?.removeEventListener('keydown', this._onKey);
61
+ }
62
+
63
+ select() {
64
+ if (this.getBoolAttr('disabled') || this.getBoolAttr('selected')) return;
65
+ this.setBoolAttr('selected', true);
66
+ this.emit('nk-change', { value: this.value, name: this.name, checked: true });
67
+ this.emit('nk-select', { value: this.value, label: this.getAttribute('title') });
68
+ this.dispatchEvent(new Event('change', { bubbles: true }));
69
+ }
70
+
71
+ onAttributeChanged(name) {
72
+ this._sync();
73
+ if (name === 'selected' && this.getBoolAttr('selected')) this._unselectPeers();
74
+ }
75
+
76
+ resetValue() { this.setBoolAttr('selected', this._defaultSelected); }
77
+ focus(o) { this._card?.focus(o); }
78
+
79
+ get selected() { return this.getBoolAttr('selected'); }
80
+ set selected(v) { this.setBoolAttr('selected', v); }
81
+ get value() { return this.getAttribute('value') || 'on'; }
82
+ set value(v) { this.setAttribute('value', v); }
83
+ get name() { return this.getAttribute('name'); }
84
+ set name(v) { this.setAttribute('name', v); }
85
+ }
86
+
87
+ customElements.define('nk-model-card', NkModelCard);
88
+ export { NkModelCard };
@@ -0,0 +1,14 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-page-actions><span>👤 Ada</span><span>📅 12 May</span><span>🏷️ <nk-tag color="purple">Design</nk-tag></span></nk-page-actions>
4
+ // → <div class="nk-page-meta">…</div> – the meta row under the title
5
+ class NkPageActions extends NkElement {
6
+ render() {
7
+ const row = this.createElement('div', ['nk-page-meta']);
8
+ row.appendChild(document.createElement('slot'));
9
+ this._wrapper.appendChild(row);
10
+ }
11
+ }
12
+
13
+ customElements.define('nk-page-actions', NkPageActions);
14
+ export { NkPageActions };
@@ -0,0 +1,34 @@
1
+ import { NkElement } from '../../base.js';
2
+
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
5
+ class NkPageCover extends NkElement {
6
+ static get observedAttributes() { return ['src']; }
7
+
8
+ render() {
9
+ this._cover = this.createElement('div', ['nk-cover']);
10
+ this._wrapper.appendChild(this._cover);
11
+ this._sync();
12
+ }
13
+
14
+ _sync() {
15
+ const src = this.getAttribute('src');
16
+ 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
+ }
25
+ }
26
+
27
+ onAttributeChanged() { this._sync(); }
28
+
29
+ get src() { return this.getAttribute('src'); }
30
+ set src(v) { v == null ? this.removeAttribute('src') : this.setAttribute('src', v); }
31
+ }
32
+
33
+ customElements.define('nk-page-cover', NkPageCover);
34
+ export { NkPageCover };
@@ -0,0 +1,70 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-page-title>NotionKit MVP</nk-page-title> → <h1 class="nk-page-title">
4
+ // <nk-page-title editable placeholder="Untitled">…</nk-page-title>
5
+ // With `editable` the text is copied into the shadow heading (contenteditable
6
+ // cannot edit slotted nodes) and nk-change fires on blur / Enter.
7
+ class NkPageTitle extends NkElement {
8
+ static get observedAttributes() { return ['editable', 'placeholder', 'value']; }
9
+ static get observesLightDom() { return true; }
10
+
11
+ render() {
12
+ this._h1 = this.createElement('h1', ['nk-page-title']);
13
+ this._slot = document.createElement('slot');
14
+ this._h1.appendChild(this._slot);
15
+ this._wrapper.appendChild(this._h1);
16
+ this._sync();
17
+ }
18
+
19
+ _sync() {
20
+ const editable = this.getBoolAttr('editable');
21
+ if (editable) {
22
+ if (this._slot.isConnected) this._slot.remove();
23
+ this._h1.setAttribute('contenteditable', 'plaintext-only');
24
+ this._h1.setAttribute('spellcheck', 'false');
25
+ this._h1.setAttribute('role', 'textbox');
26
+ this._h1.setAttribute('aria-label', 'Title');
27
+ const text = this.getAttribute('value') ?? this.textContent.trim();
28
+ if (this._h1.textContent !== text && document.activeElement !== this) this._h1.textContent = text;
29
+ this._h1.setAttribute('data-placeholder', this.getAttribute('placeholder') || '');
30
+ } else {
31
+ this._h1.removeAttribute('contenteditable');
32
+ this._h1.removeAttribute('role');
33
+ if (this.hasAttribute('value')) { if (this._slot.isConnected) this._slot.remove(); this._h1.textContent = this.getAttribute('value'); }
34
+ else if (!this._slot.isConnected) { this._h1.textContent = ''; this._h1.appendChild(this._slot); }
35
+ }
36
+ }
37
+
38
+ projectLightDom() { if (this._h1 && !this.hasAttribute('value')) this._sync(); }
39
+
40
+ setupEvents() {
41
+ this._committed = this.value;
42
+ this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this._h1.blur(); } };
43
+ this._onBlur = () => this._commit();
44
+ this._h1.addEventListener('keydown', this._onKey);
45
+ this._h1.addEventListener('blur', this._onBlur);
46
+ }
47
+
48
+ teardownEvents() {
49
+ this._h1?.removeEventListener('keydown', this._onKey);
50
+ this._h1?.removeEventListener('blur', this._onBlur);
51
+ }
52
+
53
+ _commit() {
54
+ const value = this._h1.textContent.trim();
55
+ if (value === this._committed) return;
56
+ this._committed = value;
57
+ this.emit('nk-change', { value });
58
+ }
59
+
60
+ onAttributeChanged() { this._sync(); }
61
+
62
+ get value() { return this.getBoolAttr('editable') ? (this._h1?.textContent.trim() ?? '') : (this.getAttribute('value') ?? this.textContent.trim()); }
63
+ set value(v) { this.setAttribute('value', v); }
64
+ get editable() { return this.getBoolAttr('editable'); }
65
+ set editable(v) { this.setBoolAttr('editable', v); }
66
+ focus(o) { this._h1?.focus(o); }
67
+ }
68
+
69
+ customElements.define('nk-page-title', NkPageTitle);
70
+ export { NkPageTitle };
@@ -0,0 +1,70 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-page icon="🚀" cover>
4
+ // <nk-page-title>NotionKit MVP</nk-page-title>
5
+ // <nk-page-actions>…</nk-page-actions>
6
+ // <p class="lead">…</p>
7
+ // …blocks…
8
+ // </nk-page>
9
+ // → <div class="nk-page-scroll"><div class="nk-cover"></div><div class="nk-page">
10
+ // <div class="nk-page-icon">🚀</div>…</div></div>
11
+ //
12
+ // The host is display:contents, so .nk-page-scroll is the flex child of
13
+ // .nk-main that scrolls. `narrow` drops the scroll wrapper for pages that are
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).
16
+ class NkPage extends NkElement {
17
+ static get observedAttributes() { return ['icon', 'cover', 'narrow']; }
18
+
19
+ render() {
20
+ this._page = this.createElement('div', ['nk-page']);
21
+ this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: 'Change icon' });
22
+ const iconSlot = this.createElement('slot', [], { name: 'icon' });
23
+ iconSlot.appendChild(this._icon);
24
+ this._page.appendChild(iconSlot);
25
+ this._page.appendChild(document.createElement('slot'));
26
+ this._cover = this.createElement('div', ['nk-cover']);
27
+ this._coverSlot = this.createElement('slot', [], { name: 'cover' });
28
+ this._coverSlot.appendChild(this._cover);
29
+ this._build();
30
+ }
31
+
32
+ _build() {
33
+ this._wrapper.replaceChildren();
34
+ if (this.getBoolAttr('narrow')) {
35
+ this._wrapper.append(this._coverSlot, this._page);
36
+ } else {
37
+ const scroll = this.createElement('div', ['nk-page-scroll']);
38
+ scroll.append(this._coverSlot, this._page);
39
+ this._wrapper.appendChild(scroll);
40
+ }
41
+ this._sync();
42
+ }
43
+
44
+ _sync() {
45
+ const icon = this.getAttribute('icon');
46
+ this._icon.textContent = icon || '';
47
+ this._icon.style.display = icon ? '' : 'none';
48
+ this._cover.style.display = this.getBoolAttr('cover') ? '' : 'none';
49
+ }
50
+
51
+ setupEvents() {
52
+ this._onIcon = () => this.emit('nk-action', { action: 'icon', value: this.getAttribute('icon') });
53
+ this._icon.addEventListener('click', this._onIcon);
54
+ }
55
+
56
+ teardownEvents() { this._icon?.removeEventListener('click', this._onIcon); }
57
+
58
+ onAttributeChanged(name) {
59
+ if (name === 'narrow') this._build();
60
+ else this._sync();
61
+ }
62
+
63
+ get icon() { return this.getAttribute('icon'); }
64
+ set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
65
+ get cover() { return this.getBoolAttr('cover'); }
66
+ set cover(v) { this.setBoolAttr('cover', v); }
67
+ }
68
+
69
+ customElements.define('nk-page', NkPage);
70
+ export { NkPage };
@@ -0,0 +1,24 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-profile-row avatar="MK"><nk-btn variant="secondary" small>Change photo</nk-btn></nk-profile-row>
4
+ // → <div class="nk-profile-row"><div class="big-avatar">MK</div>…</div>
5
+ class NkProfileRow extends NkElement {
6
+ static get observedAttributes() { return ['avatar']; }
7
+
8
+ render() {
9
+ const row = this.createElement('div', ['nk-profile-row']);
10
+ this._avatar = this.createElement('div', ['big-avatar']);
11
+ const s = this.createElement('slot', [], { name: 'avatar' });
12
+ s.appendChild(this._avatar);
13
+ row.appendChild(s);
14
+ row.appendChild(document.createElement('slot'));
15
+ this._wrapper.appendChild(row);
16
+ this._sync();
17
+ }
18
+
19
+ _sync() { this._avatar.textContent = this.getAttribute('avatar') || ''; }
20
+ onAttributeChanged() { this._sync(); }
21
+ }
22
+
23
+ customElements.define('nk-profile-row', NkProfileRow);
24
+ export { NkProfileRow };
@@ -0,0 +1,98 @@
1
+ import { NkFormElement } from '../../base.js';
2
+
3
+ // <nk-segmented name="range" value="week">
4
+ // <button value="week">Week</button><button value="month">Month</button>
5
+ // </nk-segmented>
6
+ // → <div class="nk-segmented">…</div> with .active on the chosen button.
7
+ // The buttons stay in the light DOM (their look comes from the ::slotted
8
+ // twins); the element sets type/role, moves .active and submits the value.
9
+ const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
10
+
11
+ class NkSegmented extends NkFormElement {
12
+ static get observedAttributes() { return ['value', 'name', 'disabled']; }
13
+
14
+ render() {
15
+ this._box = this.createElement('div', ['nk-segmented'], { role: 'radiogroup' });
16
+ this._slot = document.createElement('slot');
17
+ this._box.appendChild(this._slot);
18
+ this._wrapper.appendChild(this._box);
19
+ this._sync();
20
+ }
21
+
22
+ get buttons() { return this._slot?.assignedElements().filter(el => el.localName === 'button') ?? []; }
23
+ _valueOf(btn) { return btn.getAttribute('value') ?? btn.textContent.trim(); }
24
+
25
+ _sync() {
26
+ const buttons = this.buttons;
27
+ if (!buttons.length) return;
28
+ let value = this.getAttribute('value');
29
+ if (value === null || !buttons.some(b => this._valueOf(b) === value)) {
30
+ value = this._valueOf(buttons.find(b => b.classList.contains('active')) || buttons[0]);
31
+ }
32
+ if (this._defaultValue === undefined) this._defaultValue = value;
33
+ const disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
34
+ for (const b of buttons) {
35
+ const on = this._valueOf(b) === value;
36
+ b.type = 'button';
37
+ b.setAttribute('role', 'radio');
38
+ b.setAttribute('aria-checked', on ? 'true' : 'false');
39
+ b.classList.toggle('active', on);
40
+ b.tabIndex = on ? 0 : -1;
41
+ b.disabled = disabled;
42
+ }
43
+ this._value = value;
44
+ this.setFormValue(value);
45
+ }
46
+
47
+ setupEvents() {
48
+ this._onSlot = () => this._sync();
49
+ this._onClick = (e) => {
50
+ const btn = e.target.closest?.('button');
51
+ if (!btn || !this.buttons.includes(btn) || btn.disabled) return;
52
+ this._choose(this._valueOf(btn));
53
+ };
54
+ this._onKey = (e) => {
55
+ const dir = KEYS[e.key];
56
+ if (!dir) return;
57
+ const buttons = this.buttons;
58
+ const i = buttons.indexOf(e.target.closest?.('button'));
59
+ if (i < 0 || buttons.length < 2) return;
60
+ e.preventDefault();
61
+ const next = buttons[(i + dir + buttons.length) % buttons.length];
62
+ this._choose(this._valueOf(next));
63
+ next.focus();
64
+ };
65
+ this._slot.addEventListener('slotchange', this._onSlot);
66
+ this.addEventListener('click', this._onClick);
67
+ this.addEventListener('keydown', this._onKey);
68
+ this._sync();
69
+ }
70
+
71
+ teardownEvents() {
72
+ this._slot?.removeEventListener('slotchange', this._onSlot);
73
+ this.removeEventListener('click', this._onClick);
74
+ this.removeEventListener('keydown', this._onKey);
75
+ }
76
+
77
+ _choose(value) {
78
+ if (value === this._value) return;
79
+ this._syncing = true;
80
+ this.setAttribute('value', value);
81
+ this._syncing = false;
82
+ this._sync();
83
+ this.emit('nk-change', { value, name: this.name });
84
+ this.dispatchEvent(new Event('change', { bubbles: true }));
85
+ }
86
+
87
+ onAttributeChanged() { if (!this._syncing) this._sync(); }
88
+ resetValue() { if (this._defaultValue !== undefined) this.setAttribute('value', this._defaultValue); }
89
+ onFormDisabled(d) { this._formDisabled = d; this._sync(); }
90
+
91
+ get value() { return this._value ?? this.getAttribute('value'); }
92
+ set value(v) { this.setAttribute('value', v); }
93
+ get name() { return this.getAttribute('name'); }
94
+ set name(v) { this.setAttribute('name', v); }
95
+ }
96
+
97
+ customElements.define('nk-segmented', NkSegmented);
98
+ export { NkSegmented };
@@ -0,0 +1,30 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-skeleton height="18" width="60%"></nk-skeleton>
4
+ // <nk-skeleton lines="3" widths="100%,85%,40%"></nk-skeleton>
5
+ // → <div class="nk-skeleton" style="height:…;width:…"></div> × lines
6
+ class NkSkeleton extends NkElement {
7
+ static get observedAttributes() { return ['lines', 'height', 'width', 'widths']; }
8
+
9
+ render() { this._build(); }
10
+
11
+ _build() {
12
+ this._wrapper.replaceChildren();
13
+ const lines = Math.max(1, Number(this.getAttribute('lines')) || 1);
14
+ const height = this.getAttribute('height') || '13';
15
+ const widths = (this.getAttribute('widths') || '').split(',').map(s => s.trim()).filter(Boolean);
16
+ const width = this.getAttribute('width');
17
+ for (let i = 0; i < lines; i++) {
18
+ const line = this.createElement('div', ['nk-skeleton'], { 'aria-hidden': 'true' });
19
+ line.style.height = /^\d+(\.\d+)?$/.test(height) ? `${height}px` : height;
20
+ const w = widths[i] ?? width;
21
+ if (w) line.style.width = w;
22
+ this._wrapper.appendChild(line);
23
+ }
24
+ }
25
+
26
+ onAttributeChanged() { this._build(); }
27
+ }
28
+
29
+ customElements.define('nk-skeleton', NkSkeleton);
30
+ export { NkSkeleton };
@@ -0,0 +1,39 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-stat label="Active pages" value="128" delta="▲ 12 this week" trend="up"></nk-stat>
4
+ // → <div class="nk-stat"><div class="s-label">…</div><div class="s-value">…</div><div class="s-delta up">…</div></div>
5
+ class NkStat extends NkElement {
6
+ static get observedAttributes() { return ['label', 'value', 'delta', 'trend']; }
7
+
8
+ render() {
9
+ const box = this.createElement('div', ['nk-stat']);
10
+ this._parts = {};
11
+ for (const [slot, cls] of [['label', 's-label'], ['value', 's-value'], ['delta', 's-delta']]) {
12
+ const el = this.createElement('div', [cls]);
13
+ const s = this.createElement('slot', [], { name: slot });
14
+ s.appendChild(el);
15
+ box.appendChild(s);
16
+ this._parts[slot] = el;
17
+ }
18
+ this._wrapper.appendChild(box);
19
+ this._sync();
20
+ }
21
+
22
+ _sync() {
23
+ for (const [slot, el] of Object.entries(this._parts)) {
24
+ const v = this.getAttribute(slot);
25
+ el.textContent = v ?? '';
26
+ el.style.display = v === null ? 'none' : '';
27
+ }
28
+ const trend = this.getAttribute('trend');
29
+ this._parts.delta.className = 's-delta' + (trend === 'up' || trend === 'down' ? ` ${trend}` : '');
30
+ }
31
+
32
+ onAttributeChanged() { this._sync(); }
33
+
34
+ get value() { return this.getAttribute('value'); }
35
+ set value(v) { this.setAttribute('value', v); }
36
+ }
37
+
38
+ customElements.define('nk-stat', NkStat);
39
+ export { NkStat };
@@ -0,0 +1,13 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-stats><nk-stat …></nk-stat>…</nk-stats> → <div class="nk-stats">
4
+ class NkStats extends NkElement {
5
+ render() {
6
+ const row = this.createElement('div', ['nk-stats']);
7
+ row.appendChild(document.createElement('slot'));
8
+ this._wrapper.appendChild(row);
9
+ }
10
+ }
11
+
12
+ customElements.define('nk-stats', NkStats);
13
+ export { NkStats };
@@ -0,0 +1,25 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-synced badge="⟳ 3 places">…</nk-synced>
4
+ // → <div class="nk-synced"><span class="synced-badge">⟳ 3 places</span>…</div>
5
+ class NkSynced extends NkElement {
6
+ static get observedAttributes() { return ['badge']; }
7
+
8
+ render() {
9
+ const box = this.createElement('div', ['nk-synced']);
10
+ this._badge = this.createElement('span', ['synced-badge']);
11
+ box.appendChild(this._badge);
12
+ box.appendChild(document.createElement('slot'));
13
+ this._wrapper.appendChild(box);
14
+ this._sync();
15
+ }
16
+
17
+ _sync() { this._badge.textContent = this.getAttribute('badge') ?? '⟳ synced'; }
18
+ onAttributeChanged() { this._sync(); }
19
+
20
+ get badge() { return this.getAttribute('badge'); }
21
+ set badge(v) { this.setAttribute('badge', v); }
22
+ }
23
+
24
+ customElements.define('nk-synced', NkSynced);
25
+ export { NkSynced };
@@ -0,0 +1,53 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tab value="notes" active>📝 Notes</nk-tab> → <span class="nk-tab active" role="tab">
4
+ class NkTab extends NkElement {
5
+ static get observedAttributes() { return ['active', 'value', 'disabled']; }
6
+
7
+ render() {
8
+ this._tab = this.createElement('span', ['nk-tab'], { role: 'tab' });
9
+ this._tab.appendChild(document.createElement('slot'));
10
+ this._wrapper.appendChild(this._tab);
11
+ this._sync();
12
+ }
13
+
14
+ _sync() {
15
+ const active = this.getBoolAttr('active');
16
+ this._tab.classList.toggle('active', active);
17
+ this._tab.setAttribute('aria-selected', active ? 'true' : 'false');
18
+ this._tab.tabIndex = active ? 0 : -1;
19
+ this._tab.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
20
+ }
21
+
22
+ setupEvents() {
23
+ this._onClick = () => this.select();
24
+ this._onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); } };
25
+ this._tab.addEventListener('click', this._onClick);
26
+ this._tab.addEventListener('keydown', this._onKey);
27
+ }
28
+
29
+ teardownEvents() {
30
+ this._tab?.removeEventListener('click', this._onClick);
31
+ this._tab?.removeEventListener('keydown', this._onKey);
32
+ }
33
+
34
+ select() {
35
+ if (this.getBoolAttr('disabled')) return;
36
+ const ok = this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
37
+ if (!ok) return;
38
+ const tabs = this.closest('nk-tabs');
39
+ if (tabs) tabs.value = this.value;
40
+ else this.setBoolAttr('active', true);
41
+ }
42
+
43
+ onAttributeChanged() { this._sync(); }
44
+ focus(o) { this._tab?.focus(o); }
45
+
46
+ get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
47
+ set value(v) { this.setAttribute('value', v); }
48
+ get active() { return this.getBoolAttr('active'); }
49
+ set active(v) { this.setBoolAttr('active', v); }
50
+ }
51
+
52
+ customElements.define('nk-tab', NkTab);
53
+ export { NkTab };
@@ -0,0 +1,81 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tabs value="notes">
4
+ // <nk-tab value="notes">📝 Notes</nk-tab>
5
+ // <nk-tab value="tasks">✅ Tasks</nk-tab>
6
+ // <div slot="panel" data-tab="notes" class="nk-tab-panel">…</div>
7
+ // <div slot="panel" data-tab="tasks" class="nk-tab-panel">…</div>
8
+ // </nk-tabs>
9
+ // → <div class="nk-tabs" role="tablist">…</div> + the panels below it.
10
+ // The tabs keep one tab active and hide every panel whose data-tab does not
11
+ // match `value` (through the hidden attribute – the panels stay yours).
12
+ const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
13
+
14
+ class NkTabs extends NkElement {
15
+ static get observedAttributes() { return ['value']; }
16
+
17
+ render() {
18
+ this._strip = this.createElement('div', ['nk-tabs'], { role: 'tablist' });
19
+ this._tabSlot = document.createElement('slot');
20
+ this._strip.appendChild(this._tabSlot);
21
+ this._panelSlot = this.createElement('slot', [], { name: 'panel' });
22
+ this._wrapper.append(this._strip, this._panelSlot);
23
+ this._sync();
24
+ }
25
+
26
+ get tabs() { return [...this.querySelectorAll(':scope > nk-tab')]; }
27
+ get panels() { return [...this.querySelectorAll(':scope > [slot="panel"]')]; }
28
+
29
+ _sync() {
30
+ const tabs = this.tabs;
31
+ if (!tabs.length) return;
32
+ let value = this.getAttribute('value');
33
+ if (value === null || !tabs.some(t => t.getAttribute('value') === value || (t.getAttribute('value') === null && t.textContent.trim() === value))) {
34
+ const preset = tabs.find(t => t.hasAttribute('active')) || tabs[0];
35
+ value = preset.getAttribute('value') ?? preset.textContent.trim();
36
+ }
37
+ for (const tab of tabs) {
38
+ const v = tab.getAttribute('value') ?? tab.textContent.trim();
39
+ if (v === value) tab.setAttribute('active', ''); else tab.removeAttribute('active');
40
+ }
41
+ for (const panel of this.panels) panel.hidden = panel.dataset.tab !== value;
42
+ this._value = value;
43
+ }
44
+
45
+ setupEvents() {
46
+ this._onSlot = () => this._sync();
47
+ this._onKey = (e) => {
48
+ const dir = KEYS[e.key];
49
+ if (!dir) return;
50
+ const tabs = this.tabs.filter(t => !t.hasAttribute('disabled'));
51
+ const i = tabs.indexOf(e.target.closest?.('nk-tab'));
52
+ if (i < 0 || tabs.length < 2) return;
53
+ e.preventDefault();
54
+ const next = tabs[(i + dir + tabs.length) % tabs.length];
55
+ next.select();
56
+ next.focus();
57
+ };
58
+ this._tabSlot.addEventListener('slotchange', this._onSlot);
59
+ this._panelSlot.addEventListener('slotchange', this._onSlot);
60
+ this.addEventListener('keydown', this._onKey);
61
+ this._sync();
62
+ }
63
+
64
+ teardownEvents() {
65
+ this._tabSlot?.removeEventListener('slotchange', this._onSlot);
66
+ this._panelSlot?.removeEventListener('slotchange', this._onSlot);
67
+ this.removeEventListener('keydown', this._onKey);
68
+ }
69
+
70
+ onAttributeChanged() {
71
+ const before = this._value;
72
+ this._sync();
73
+ if (this._value !== before) this.emit('nk-change', { value: this._value });
74
+ }
75
+
76
+ get value() { return this._value ?? this.getAttribute('value'); }
77
+ set value(v) { this.setAttribute('value', v); }
78
+ }
79
+
80
+ customElements.define('nk-tabs', NkTabs);
81
+ export { NkTabs };