@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,42 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-template-btn icon="📅" value="week-plan">Insert week plan</nk-template-btn>
4
+ // → <button class="nk-template-btn">📅 Insert week plan</button>
5
+ class NkTemplateBtn extends NkElement {
6
+ static get observedAttributes() { return ['icon', 'value', 'disabled']; }
7
+
8
+ render() {
9
+ this._btn = this.createElement('button', ['nk-template-btn'], { type: 'button' });
10
+ // One flex item: "📅 Insert week plan" is a single text run in the class
11
+ // markup, so icon, space and label must not become separate flex items.
12
+ const run = document.createElement('span');
13
+ this._icon = document.createElement('span');
14
+ this._space = document.createTextNode(' ');
15
+ run.append(this._icon, this._space, document.createElement('slot'));
16
+ this._btn.appendChild(run);
17
+ this._wrapper.appendChild(this._btn);
18
+ this._sync();
19
+ }
20
+
21
+ _sync() {
22
+ const icon = this.getAttribute('icon');
23
+ this._icon.textContent = icon || '';
24
+ this._space.data = icon ? ' ' : '';
25
+ this._btn.disabled = this.getBoolAttr('disabled');
26
+ }
27
+
28
+ setupEvents() {
29
+ this._onClick = () => this.emit('nk-select', { value: this.value, label: this.textContent.trim() });
30
+ this._btn.addEventListener('click', this._onClick);
31
+ }
32
+
33
+ teardownEvents() { this._btn?.removeEventListener('click', this._onClick); }
34
+ onAttributeChanged() { this._sync(); }
35
+ focus(o) { this._btn?.focus(o); }
36
+
37
+ get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
38
+ set value(v) { this.setAttribute('value', v); }
39
+ }
40
+
41
+ customElements.define('nk-template-btn', NkTemplateBtn);
42
+ export { NkTemplateBtn };
@@ -0,0 +1,22 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-app>
4
+ // <nk-sidebar slot="sidebar">…</nk-sidebar>
5
+ // <nk-topbar>…</nk-topbar>
6
+ // <nk-page>…</nk-page>
7
+ // </nk-app>
8
+ // → <div class="nk-app"><aside class="nk-sidebar">…</aside><main class="nk-main">…</main></div>
9
+ // The outermost element of a workspace app: a full-height flex row.
10
+ class NkApp extends NkElement {
11
+ render() {
12
+ const app = this.createElement('div', ['nk-app']);
13
+ app.appendChild(this.createElement('slot', [], { name: 'sidebar' }));
14
+ const main = this.createElement('main', ['nk-main']);
15
+ main.appendChild(document.createElement('slot'));
16
+ app.appendChild(main);
17
+ this._wrapper.appendChild(app);
18
+ }
19
+ }
20
+
21
+ customElements.define('nk-app', NkApp);
22
+ export { NkApp };
@@ -0,0 +1,77 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-breadcrumb>
4
+ // <a href="/overview">📊 Project overview</a>
5
+ // <span>🚀 NotionKit MVP</span>
6
+ // </nk-breadcrumb>
7
+ // → <nav class="nk-breadcrumb"><a class="crumb">…</a><span class="sep">/</span><span class="crumb current">…</span></nav>
8
+ //
9
+ // The children are *cloned* into the shadow tree so the separators can be
10
+ // interleaved and `.crumb:hover` (no ::slotted twin) applies. A
11
+ // MutationObserver keeps the copy in step. Clicking a clone fires nk-select
12
+ // and, unless prevented, forwards the click to the original – so an <a>
13
+ // navigates exactly once and framework listeners see it.
14
+ class NkBreadcrumb extends NkElement {
15
+ static get observedAttributes() { return ['separator']; }
16
+ static get observesLightDom() { return true; }
17
+
18
+ render() {
19
+ this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': 'Breadcrumb' });
20
+ this._wrapper.appendChild(this._nav);
21
+ this.projectLightDom();
22
+ }
23
+
24
+ projectLightDom() {
25
+ if (!this._nav) return;
26
+ const originals = [...this.children];
27
+ const sep = this.getAttribute('separator') ?? '/';
28
+ const signature = originals.map(o => o.outerHTML).join('') + sep;
29
+ if (signature === this._signature) return;
30
+ this._signature = signature;
31
+ this._nav.innerHTML = '';
32
+ const explicitCurrent = originals.some(o => o.hasAttribute('current') || o.classList.contains('current'));
33
+ originals.forEach((original, i) => {
34
+ if (i > 0) {
35
+ const s = this.createElement('span', ['sep'], { 'aria-hidden': 'true' });
36
+ s.textContent = sep;
37
+ this._nav.appendChild(s);
38
+ }
39
+ const clone = original.cloneNode(true);
40
+ clone.removeAttribute('slot');
41
+ clone.classList.add('crumb');
42
+ const current = explicitCurrent ? (original.hasAttribute('current') || original.classList.contains('current')) : i === originals.length - 1;
43
+ clone.classList.toggle('current', current);
44
+ if (current) clone.setAttribute('aria-current', 'page');
45
+ clone.dataset.index = i;
46
+ this._nav.appendChild(clone);
47
+ });
48
+ }
49
+
50
+ setupEvents() {
51
+ this._onClick = (e) => {
52
+ const clone = e.target.closest('.crumb');
53
+ if (!clone) return;
54
+ e.preventDefault();
55
+ const index = Number(clone.dataset.index);
56
+ // Look the original up at click time: a framework may have replaced it.
57
+ const original = this.children[index];
58
+ const ok = this.emit('nk-select', {
59
+ index, label: clone.textContent.trim(), href: original?.getAttribute('href') ?? null,
60
+ value: original?.dataset.value ?? original?.getAttribute('href') ?? clone.textContent.trim(),
61
+ current: clone.classList.contains('current'),
62
+ });
63
+ if (ok) original?.click();
64
+ };
65
+ this._nav.addEventListener('click', this._onClick);
66
+ }
67
+
68
+ teardownEvents() { this._nav?.removeEventListener('click', this._onClick); }
69
+
70
+ onAttributeChanged() { this.projectLightDom(); }
71
+
72
+ get separator() { return this.getAttribute('separator') ?? '/'; }
73
+ set separator(v) { this.setAttribute('separator', v); }
74
+ }
75
+
76
+ customElements.define('nk-breadcrumb', NkBreadcrumb);
77
+ export { NkBreadcrumb };
@@ -0,0 +1,41 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-section-label addable>Favourites</nk-section-label>
4
+ // → <div class="nk-section-label">Favourites <span class="plus">+</span></div>
5
+ // The + appears on hover (a hover-parent rule, so it lives in the same tree)
6
+ // and fires nk-action { action: 'add' }.
7
+ class NkSectionLabel extends NkElement {
8
+ static get observedAttributes() { return ['label', 'addable']; }
9
+
10
+ render() {
11
+ this._box = this.createElement('div', ['nk-section-label']);
12
+ this._text = document.createElement('span');
13
+ this._text.appendChild(document.createElement('slot'));
14
+ this._plus = this.createElement('span', ['plus'], { role: 'button', title: '+' });
15
+ this._plus.textContent = '+';
16
+ this._box.append(this._text, this._plus);
17
+ this._wrapper.appendChild(this._box);
18
+ this._sync();
19
+ }
20
+
21
+ _sync() {
22
+ const label = this.getAttribute('label');
23
+ if (label !== null) this._text.textContent = label;
24
+ this._plus.style.display = this.getBoolAttr('addable') ? '' : 'none';
25
+ }
26
+
27
+ setupEvents() {
28
+ this._onPlus = (e) => { e.stopPropagation(); this.emit('nk-action', { action: 'add' }); };
29
+ this._plus.addEventListener('click', this._onPlus);
30
+ }
31
+
32
+ teardownEvents() { this._plus?.removeEventListener('click', this._onPlus); }
33
+
34
+ onAttributeChanged() { this._sync(); }
35
+
36
+ get addable() { return this.getBoolAttr('addable'); }
37
+ set addable(v) { this.setBoolAttr('addable', v); }
38
+ }
39
+
40
+ customElements.define('nk-section-label', NkSectionLabel);
41
+ export { NkSectionLabel };
@@ -0,0 +1,80 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-sidebar slot="sidebar">
4
+ // <nk-workspace-switcher slot="workspace" name="Acme"></nk-workspace-switcher>
5
+ // <nk-tree>…</nk-tree>
6
+ // <nk-tree-item slot="footer" icon="⚙️">Settings</nk-tree-item>
7
+ // </nk-sidebar>
8
+ // → <aside class="nk-sidebar"><div class="nk-workspace">…</div>
9
+ // <div class="nk-sidebar-scroll">…</div><div class="nk-sidebar-footer">…</div></aside>
10
+ //
11
+ // The host is display:contents so the <aside> is a direct flex child of
12
+ // .nk-app, exactly like the class markup. Below 860px notionkit.css hides the
13
+ // aside; `open` brings it back as an off-canvas drawer (position only – the
14
+ // look is still the stylesheet's).
15
+ const drawerSheet = new CSSStyleSheet();
16
+ drawerSheet.replaceSync(`
17
+ @media (max-width: 860px) {
18
+ :host([open]) .nk-sidebar { display: flex; position: fixed; inset: 0 auto 0 0; z-index: 60; }
19
+ :host([open]) .nk-sidebar-backdrop { display: block; }
20
+ }
21
+ .nk-sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 59; background: var(--nk-scrim-soft); }
22
+ `);
23
+
24
+ class NkSidebar extends NkElement {
25
+ static get hostStyles() { return drawerSheet; }
26
+ static get observedAttributes() { return ['open']; }
27
+
28
+ render() {
29
+ this._backdrop = this.createElement('div', ['nk-sidebar-backdrop']);
30
+ this._aside = this.createElement('aside', ['nk-sidebar']);
31
+ this._aside.appendChild(this.createElement('slot', [], { name: 'workspace' }));
32
+ const scroll = this.createElement('div', ['nk-sidebar-scroll']);
33
+ scroll.appendChild(document.createElement('slot'));
34
+ this._aside.appendChild(scroll);
35
+ this._footer = this.createElement('div', ['nk-sidebar-footer']);
36
+ this._footerSlot = this.createElement('slot', [], { name: 'footer' });
37
+ this._footer.appendChild(this._footerSlot);
38
+ this._aside.appendChild(this._footer);
39
+ this._wrapper.appendChild(this._backdrop);
40
+ this._wrapper.appendChild(this._aside);
41
+ this._syncFooter();
42
+ }
43
+
44
+ _syncFooter() {
45
+ const nodes = this._footerSlot.assignedElements();
46
+ // The footer box carries padding and a border; only render it when used.
47
+ this._footer.style.display = nodes.length ? '' : 'none';
48
+ // Footer rows are 26px in the class version (.nk-sidebar-footer .nk-tree-item).
49
+ for (const el of nodes) if (el.localName === 'nk-tree-item') el.setAttribute('compact', '');
50
+ }
51
+
52
+ setupEvents() {
53
+ this._onSlot = () => this._syncFooter();
54
+ this._onBackdrop = () => this.close();
55
+ this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
56
+ this._footerSlot.addEventListener('slotchange', this._onSlot);
57
+ this._backdrop.addEventListener('click', this._onBackdrop);
58
+ document.addEventListener('keydown', this._onKey);
59
+ }
60
+
61
+ teardownEvents() {
62
+ this._footerSlot?.removeEventListener('slotchange', this._onSlot);
63
+ this._backdrop?.removeEventListener('click', this._onBackdrop);
64
+ document.removeEventListener('keydown', this._onKey);
65
+ }
66
+
67
+ onAttributeChanged(name) {
68
+ if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open') });
69
+ }
70
+
71
+ show() { this.setBoolAttr('open', true); }
72
+ close() { this.setBoolAttr('open', false); }
73
+ toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
74
+
75
+ get open() { return this.getBoolAttr('open'); }
76
+ set open(v) { this.setBoolAttr('open', v); }
77
+ }
78
+
79
+ customElements.define('nk-sidebar', NkSidebar);
80
+ export { NkSidebar };
@@ -0,0 +1,66 @@
1
+ import { NkElement, getCurrentTheme } from '../../base.js';
2
+
3
+ // <nk-theme-toggle slot="actions"></nk-theme-toggle>
4
+ // → <button class="nk-topbar-btn nk-theme-toggle">🌙</button>
5
+ // Flips data-theme on <html> (the only theme source), remembers the choice in
6
+ // localStorage (`storage-key`, default nk-theme), applies a stored or
7
+ // system preference on first connect when <html> carries no theme yet, and
8
+ // accepts postMessage({ nkTheme }) from a parent page (the landing page's
9
+ // embedded demo uses that).
10
+ class NkThemeToggle extends NkElement {
11
+ static get observedAttributes() { return ['storage-key', 'title'];
12
+
13
+ }
14
+
15
+ render() {
16
+ this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
17
+ this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
18
+ this._wrapper.appendChild(this._btn);
19
+ const root = document.documentElement;
20
+ let stored = null;
21
+ try { stored = localStorage.getItem(this.storageKey); } catch {}
22
+ if (stored === 'light' || stored === 'dark') root.setAttribute('data-theme', stored);
23
+ else if (!root.hasAttribute('data-theme')) root.setAttribute('data-theme', matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
24
+ this._syncGlyph();
25
+ }
26
+
27
+ _syncGlyph() {
28
+ if (this._btn) this._btn.textContent = getCurrentTheme() === 'dark' ? '☀️' : '🌙';
29
+ }
30
+
31
+ _syncTheme(theme) {
32
+ super._syncTheme(theme);
33
+ this._syncGlyph();
34
+ }
35
+
36
+ setupEvents() {
37
+ this._onClick = () => this.apply(getCurrentTheme() === 'dark' ? 'light' : 'dark');
38
+ this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark') this.apply(t); };
39
+ this._btn.addEventListener('click', this._onClick);
40
+ window.addEventListener('message', this._onMessage);
41
+ }
42
+
43
+ teardownEvents() {
44
+ this._btn?.removeEventListener('click', this._onClick);
45
+ window.removeEventListener('message', this._onMessage);
46
+ }
47
+
48
+ onAttributeChanged(name) {
49
+ if (name === 'title' && this._btn) this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
50
+ }
51
+
52
+ /** Sets the theme on <html>, persists it and reports it. */
53
+ apply(theme) {
54
+ document.documentElement.setAttribute('data-theme', theme);
55
+ try { localStorage.setItem(this.storageKey, theme); } catch {}
56
+ this._syncGlyph();
57
+ this.emit('nk-change', { value: theme });
58
+ }
59
+
60
+ get storageKey() { return this.getAttribute('storage-key') || 'nk-theme'; }
61
+ get value() { return getCurrentTheme(); }
62
+ set value(v) { this.apply(v); }
63
+ }
64
+
65
+ customElements.define('nk-theme-toggle', NkThemeToggle);
66
+ export { NkThemeToggle };
@@ -0,0 +1,23 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-topbar>
4
+ // <nk-breadcrumb>…</nk-breadcrumb>
5
+ // <nk-btn slot="actions" variant="share">Share</nk-btn>
6
+ // <nk-theme-toggle slot="actions"></nk-theme-toggle>
7
+ // </nk-topbar>
8
+ // → <div class="nk-topbar">…<div class="nk-topbar-actions">…</div></div>
9
+ // display:contents host, so the bar is a direct flex child of .nk-main.
10
+ class NkTopbar extends NkElement {
11
+
12
+ render() {
13
+ const bar = this.createElement('div', ['nk-topbar']);
14
+ bar.appendChild(document.createElement('slot'));
15
+ const actions = this.createElement('div', ['nk-topbar-actions']);
16
+ actions.appendChild(this.createElement('slot', [], { name: 'actions' }));
17
+ bar.appendChild(actions);
18
+ this._wrapper.appendChild(bar);
19
+ }
20
+ }
21
+
22
+ customElements.define('nk-topbar', NkTopbar);
23
+ export { NkTopbar };
@@ -0,0 +1,154 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tree-item icon="📊" value="overview" open>
4
+ // Project overview
5
+ // <nk-tree-item icon="🚀" value="mvp" active>NotionKit MVP</nk-tree-item>
6
+ // </nk-tree-item>
7
+ //
8
+ // → <div class="nk-tree-item active"><span class="nk-toggle-arrow open">▸</span>
9
+ // <span class="icon">📊</span><span class="label">…</span>
10
+ // <span class="actions"><span>+</span><span>⋯</span></span></div>
11
+ // <div class="nk-tree-children">…</div>
12
+ //
13
+ // Both halves – the row and the .nk-tree-children box that must be its next
14
+ // sibling – are rendered here. Slots are assigned manually: text becomes the
15
+ // label, nested <nk-tree-item>s become the children, `slot="icon"` /
16
+ // `slot="end"` nodes go where they say. The hover actions are shadow-internal
17
+ // (`.nk-tree-item:hover .actions` is a hover-parent rule) and report through
18
+ // nk-action.
19
+ class NkTreeItem extends NkElement {
20
+ static get slotAssignment() { return 'manual'; }
21
+ static get observedAttributes() { return ['label', 'icon', 'value', 'href', 'active', 'open', 'compact', 'no-actions']; }
22
+
23
+ render() {
24
+ this._row = this.createElement('div', ['nk-tree-item'], { role: 'treeitem', tabindex: '-1' });
25
+ this._arrow = this.createElement('span', ['nk-toggle-arrow'], { 'aria-hidden': 'true' });
26
+ this._arrow.textContent = '▸';
27
+ this._iconSlot = this.createElement('slot', [], { name: 'icon' });
28
+ this._icon = this.createElement('span', ['icon']);
29
+ this._iconSlot.appendChild(this._icon);
30
+ this._label = this.createElement('span', ['label']);
31
+ this._labelSlot = this.createElement('slot', [], { name: 'label' });
32
+ this._labelText = document.createTextNode('');
33
+ this._labelSlot.appendChild(this._labelText);
34
+ this._label.appendChild(this._labelSlot);
35
+ this._actions = this.createElement('span', ['actions']);
36
+ for (const [action, glyph, title] of [['add', '+', 'Add'], ['more', '⋯', 'More']]) {
37
+ const b = this.createElement('span', [], { 'data-action': action, role: 'button', title });
38
+ b.textContent = glyph;
39
+ this._actions.appendChild(b);
40
+ }
41
+ this._endSlot = this.createElement('slot', [], { name: 'end' });
42
+ this._row.append(this._arrow, this._iconSlot, this._label, this._actions, this._endSlot);
43
+ this._children = this.createElement('div', ['nk-tree-children'], { role: 'group' });
44
+ this._childSlot = this.createElement('slot', [], { name: 'children' });
45
+ this._children.appendChild(this._childSlot);
46
+ this._wrapper.append(this._row, this._children);
47
+ this._sync();
48
+ this.closest('nk-tree')?.requestSync?.();
49
+ }
50
+
51
+ /** Routes light-DOM nodes: nested items → children, slot="icon"/"end" → there, the rest → label. */
52
+ assignSlots() {
53
+ if (!this._row) return;
54
+ const icon = [], end = [], children = [], label = [];
55
+ for (const node of this.childNodes) {
56
+ if (node.nodeType === Node.ELEMENT_NODE) {
57
+ const slot = node.getAttribute('slot');
58
+ if (slot === 'icon') icon.push(node);
59
+ else if (slot === 'end') end.push(node);
60
+ else if (node.localName === 'nk-tree-item') children.push(node);
61
+ else label.push(node);
62
+ } else if (node.nodeType === Node.TEXT_NODE && node.data.trim()) {
63
+ label.push(node);
64
+ }
65
+ }
66
+ this._iconSlot.assign(...icon);
67
+ this._endSlot.assign(...end);
68
+ this._childSlot.assign(...children);
69
+ this._labelSlot.assign(...(this.hasAttribute('label') ? [] : label));
70
+ this._hasChildren = children.length > 0;
71
+ this._hasEnd = end.length > 0;
72
+ this._sync();
73
+ }
74
+
75
+ _sync() {
76
+ const active = this.getBoolAttr('active'), open = this.getBoolAttr('open');
77
+ this._row.classList.toggle('active', active);
78
+ this._row.classList.toggle('compact', this.getBoolAttr('compact'));
79
+ this._row.setAttribute('aria-selected', active ? 'true' : 'false');
80
+ this._arrow.classList.toggle('open', open);
81
+ this._arrow.style.display = this._hasChildren ? '' : 'none';
82
+ if (this._hasChildren) this._row.setAttribute('aria-expanded', open ? 'true' : 'false');
83
+ else this._row.removeAttribute('aria-expanded');
84
+ this._children.classList.toggle('collapsed', !open || !this._hasChildren);
85
+ this._icon.textContent = this.getAttribute('icon') || '';
86
+ this._icon.style.display = this.getAttribute('icon') ? '' : 'none';
87
+ this._labelText.data = this.getAttribute('label') || '';
88
+ // .actions is display:none until hover (a hover-parent rule); `hidden`
89
+ // would lose to that rule, so the block is detached instead.
90
+ const wantActions = !this.getBoolAttr('no-actions') && !this._hasEnd;
91
+ if (wantActions && !this._actions.isConnected) this._row.insertBefore(this._actions, this._endSlot);
92
+ if (!wantActions && this._actions.isConnected) this._actions.remove();
93
+ const href = this.getAttribute('href');
94
+ href ? this._row.setAttribute('data-href', href) : this._row.removeAttribute('data-href');
95
+ }
96
+
97
+ setupEvents() {
98
+ this._onRowClick = (e) => {
99
+ const action = e.target.closest?.('[data-action]');
100
+ if (action) { e.stopPropagation(); this.emit('nk-action', { action: action.dataset.action, value: this.value, label: this.label }); return; }
101
+ if (e.target === this._arrow || this._arrow.contains(e.target)) { e.stopPropagation(); this.toggle(); return; }
102
+ this.select();
103
+ };
104
+ this._onKey = (e) => {
105
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); }
106
+ };
107
+ this._row.addEventListener('click', this._onRowClick);
108
+ this._row.addEventListener('keydown', this._onKey);
109
+ }
110
+
111
+ teardownEvents() {
112
+ this._row?.removeEventListener('click', this._onRowClick);
113
+ this._row?.removeEventListener('keydown', this._onKey);
114
+ }
115
+
116
+ onAttributeChanged(name) {
117
+ if (name === 'label') this.assignSlots();
118
+ else this._sync();
119
+ if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open'), value: this.value });
120
+ }
121
+
122
+ /** Fires nk-select; a listener may preventDefault to keep the item from becoming active. */
123
+ select() {
124
+ const ok = this.emit('nk-select', { value: this.value, label: this.label, href: this.getAttribute('href'), item: this });
125
+ if (!ok) return;
126
+ const tree = this.closest('nk-tree');
127
+ if (tree) tree.setActive(this);
128
+ else this.setBoolAttr('active', true);
129
+ if (this.getAttribute('href')) location.assign(this.getAttribute('href'));
130
+ }
131
+
132
+ toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
133
+ focus(options) { this._row?.focus(options); }
134
+ /** Roving tabindex, managed by <nk-tree>. */
135
+ setTabbable(v) { if (this._row) this._row.tabIndex = v ? 0 : -1; }
136
+ get tabbable() { return this._row?.tabIndex === 0; }
137
+ get hasChildren() { return !!this._hasChildren; }
138
+
139
+ get label() { return this.getAttribute('label') ?? this._labelSlot?.assignedNodes().map(n => n.textContent).join('').trim() ?? ''; }
140
+ set label(v) { this.setAttribute('label', v); }
141
+ get value() { return this.getAttribute('value') ?? this.label; }
142
+ set value(v) { this.setAttribute('value', v); }
143
+ get icon() { return this.getAttribute('icon'); }
144
+ set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
145
+ get active() { return this.getBoolAttr('active'); }
146
+ set active(v) { this.setBoolAttr('active', v); }
147
+ get open() { return this.getBoolAttr('open'); }
148
+ set open(v) { this.setBoolAttr('open', v); }
149
+ get compact() { return this.getBoolAttr('compact'); }
150
+ set compact(v) { this.setBoolAttr('compact', v); }
151
+ }
152
+
153
+ customElements.define('nk-tree-item', NkTreeItem);
154
+ export { NkTreeItem };
@@ -0,0 +1,122 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tree>
4
+ // <nk-section-label addable>Favourites</nk-section-label>
5
+ // <nk-tree-item icon="📊" open>Project overview
6
+ // <nk-tree-item icon="🚀" active>NotionKit MVP</nk-tree-item>
7
+ // </nk-tree-item>
8
+ // </nk-tree>
9
+ //
10
+ // A container for tree items: keeps a single `active` item (listening to
11
+ // nk-select from any depth), gives the group one tab stop with arrow-key
12
+ // navigation, and can render items from data:
13
+ // tree.data = [{ label, icon, value, href, open, active, children: [...] }]
14
+ const KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']);
15
+
16
+ class NkTree extends NkElement {
17
+ static get observedAttributes() { return ['manual']; }
18
+
19
+ render() {
20
+ const box = this.createElement('div', [], { role: 'tree' });
21
+ box.appendChild(document.createElement('slot'));
22
+ this._wrapper.appendChild(box);
23
+ this._box = box;
24
+ }
25
+
26
+ /** Makes `item` the single active item (called by the item after nk-select was not cancelled). */
27
+ setActive(item) {
28
+ if (this.getBoolAttr('manual')) return;
29
+ for (const other of this.querySelectorAll('nk-tree-item[active]')) if (other !== item) other.removeAttribute('active');
30
+ item.setAttribute('active', '');
31
+ this._syncTabIndex(item);
32
+ }
33
+
34
+ setupEvents() {
35
+ this._onKey = (e) => {
36
+ if (!KEYS.has(e.key) || e.ctrlKey || e.metaKey || e.altKey) return;
37
+ const item = e.target.closest?.('nk-tree-item');
38
+ if (!item) return;
39
+ const visible = this.visibleItems();
40
+ const i = visible.indexOf(item);
41
+ if (i < 0) return;
42
+ let next = null;
43
+ switch (e.key) {
44
+ case 'ArrowDown': next = visible[i + 1]; break;
45
+ case 'ArrowUp': next = visible[i - 1]; break;
46
+ case 'Home': next = visible[0]; break;
47
+ case 'End': next = visible[visible.length - 1]; break;
48
+ case 'ArrowRight':
49
+ if (item.hasChildren && !item.open) item.open = true;
50
+ else if (item.hasChildren) next = item.querySelector('nk-tree-item');
51
+ break;
52
+ case 'ArrowLeft':
53
+ if (item.hasChildren && item.open) item.open = false;
54
+ else next = item.parentElement?.closest('nk-tree-item');
55
+ break;
56
+ }
57
+ e.preventDefault();
58
+ if (next) { this._syncTabIndex(next); next.focus(); }
59
+ };
60
+ this._onSlot = () => this._syncTabIndex();
61
+ this.addEventListener('keydown', this._onKey);
62
+ this._box.querySelector('slot').addEventListener('slotchange', this._onSlot);
63
+ this._syncTabIndex();
64
+ }
65
+
66
+ teardownEvents() {
67
+ this.removeEventListener('keydown', this._onKey);
68
+ this._box?.querySelector('slot').removeEventListener('slotchange', this._onSlot);
69
+ }
70
+
71
+ /** Items whose every ancestor item inside this tree is open – i.e. rendered. */
72
+ visibleItems() {
73
+ return [...this.querySelectorAll('nk-tree-item')].filter(item => {
74
+ for (let p = item.parentElement?.closest('nk-tree-item'); p && this.contains(p); p = p.parentElement?.closest('nk-tree-item')) {
75
+ if (!p.open) return false;
76
+ }
77
+ return true;
78
+ });
79
+ }
80
+
81
+ /** Items upgrade after the tree; each one asks for a resync once rendered. */
82
+ requestSync() {
83
+ if (this._syncQueued) return;
84
+ this._syncQueued = true;
85
+ queueMicrotask(() => { this._syncQueued = false; this._syncTabIndex(); });
86
+ }
87
+
88
+ _syncTabIndex(focusable) {
89
+ const items = [...this.querySelectorAll('nk-tree-item')];
90
+ if (!items.length) return;
91
+ const visible = this.visibleItems();
92
+ const target = focusable || visible.find(i => i.active) || visible[0];
93
+ // A method call, not a property write: assigning a property to an element
94
+ // that has not been upgraded yet would create an own property that shadows
95
+ // the accessor forever.
96
+ for (const item of items) item.setTabbable?.(item === target);
97
+ }
98
+
99
+ /** Renders items from data into the light DOM (replacing existing items). */
100
+ set data(list) {
101
+ this._data = list;
102
+ const build = (nodes) => nodes.map(n => {
103
+ const item = document.createElement('nk-tree-item');
104
+ if (n.icon) item.setAttribute('icon', n.icon);
105
+ if (n.value != null) item.setAttribute('value', n.value);
106
+ if (n.href) item.setAttribute('href', n.href);
107
+ if (n.open) item.setAttribute('open', '');
108
+ if (n.active) item.setAttribute('active', '');
109
+ item.append(document.createTextNode(n.label ?? ''), ...build(n.children || []));
110
+ return item;
111
+ });
112
+ for (const old of this.querySelectorAll(':scope > nk-tree-item')) old.remove();
113
+ this.append(...build(list || []));
114
+ }
115
+ get data() { return this._data; }
116
+
117
+ get activeItem() { return this.querySelector('nk-tree-item[active]'); }
118
+ get value() { return this.activeItem?.value ?? null; }
119
+ }
120
+
121
+ customElements.define('nk-tree', NkTree);
122
+ export { NkTree };