@jungherz-de/notionkit-elements 1.0.1 → 1.2.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 (90) hide show
  1. package/README.md +11 -11
  2. package/dist/components/nk-ai-input-row.js +1 -1
  3. package/dist/components/nk-ai-msg.js +1 -1
  4. package/dist/components/nk-ai-thread.js +1 -1
  5. package/dist/components/nk-app.js +1 -1
  6. package/dist/components/nk-avatar-group.js +1 -1
  7. package/dist/components/nk-banner.js +1 -1
  8. package/dist/components/nk-block-host.js +1 -1
  9. package/dist/components/nk-board-view.js +1 -1
  10. package/dist/components/nk-breadcrumb.js +1 -1
  11. package/dist/components/nk-btn.js +1 -1
  12. package/dist/components/nk-callout.js +1 -1
  13. package/dist/components/nk-check.js +9 -3
  14. package/dist/components/nk-cmdk.js +1 -1
  15. package/dist/components/nk-code.js +1 -1
  16. package/dist/components/nk-comment.js +1 -1
  17. package/dist/components/nk-comments.js +1 -1
  18. package/dist/components/nk-danger-zone.js +1 -1
  19. package/dist/components/nk-database.js +1 -1
  20. package/dist/components/nk-divider.js +1 -1
  21. package/dist/components/nk-emoji-picker.js +1 -1
  22. package/dist/components/nk-empty.js +1 -1
  23. package/dist/components/nk-field.js +45 -7
  24. package/dist/components/nk-fields.js +22 -0
  25. package/dist/components/nk-filter-bar.js +1 -1
  26. package/dist/components/nk-heading.js +1 -1
  27. package/dist/components/nk-input.js +2 -2
  28. package/dist/components/nk-kbd.js +1 -1
  29. package/dist/components/nk-member-list.js +1 -1
  30. package/dist/components/nk-member-row.js +1 -1
  31. package/dist/components/nk-mention.js +1 -1
  32. package/dist/components/nk-menu-item.js +1 -1
  33. package/dist/components/nk-menu.js +1 -1
  34. package/dist/components/nk-modal.js +1 -1
  35. package/dist/components/nk-model-card.js +1 -1
  36. package/dist/components/nk-page-actions.js +1 -1
  37. package/dist/components/nk-page-cover.js +1 -1
  38. package/dist/components/nk-page-title.js +1 -1
  39. package/dist/components/nk-page.js +1 -1
  40. package/dist/components/nk-pop.js +1 -1
  41. package/dist/components/nk-profile-row.js +1 -1
  42. package/dist/components/nk-progress.js +1 -1
  43. package/dist/components/nk-quote.js +1 -1
  44. package/dist/components/nk-radio.js +9 -3
  45. package/dist/components/nk-section-label.js +1 -1
  46. package/dist/components/nk-segmented.js +1 -1
  47. package/dist/components/nk-select.js +6 -3
  48. package/dist/components/nk-settings-pane.js +1 -1
  49. package/dist/components/nk-settings-user.js +1 -1
  50. package/dist/components/nk-sidebar.js +1 -1
  51. package/dist/components/nk-skeleton.js +1 -1
  52. package/dist/components/nk-slider.js +1 -1
  53. package/dist/components/nk-stat.js +1 -1
  54. package/dist/components/nk-stats.js +1 -1
  55. package/dist/components/nk-switch.js +32 -4
  56. package/dist/components/nk-synced.js +1 -1
  57. package/dist/components/nk-tab-bar-item.js +83 -0
  58. package/dist/components/nk-tab-bar.js +83 -0
  59. package/dist/components/nk-tab.js +1 -1
  60. package/dist/components/nk-table-view.js +1 -1
  61. package/dist/components/nk-tabs.js +1 -1
  62. package/dist/components/nk-tag.js +1 -1
  63. package/dist/components/nk-template-btn.js +1 -1
  64. package/dist/components/nk-textarea.js +2 -2
  65. package/dist/components/nk-theme-toggle.js +1 -1
  66. package/dist/components/nk-toast.js +1 -1
  67. package/dist/components/nk-todo.js +1 -1
  68. package/dist/components/nk-toggle.js +1 -1
  69. package/dist/components/nk-topbar.js +1 -1
  70. package/dist/components/nk-tree-item.js +1 -1
  71. package/dist/components/nk-tree.js +1 -1
  72. package/dist/components/nk-workspace-switcher.js +1 -1
  73. package/dist/components/shared/base-EVtzqKmc.js +347 -0
  74. package/dist/notionkit-elements.esm.js +311 -21
  75. package/dist/notionkit-elements.js +313 -20
  76. package/dist/notionkit-elements.min.js +1 -1
  77. package/package.json +3 -3
  78. package/src/base.js +31 -1
  79. package/src/components/forms/nk-check.js +8 -2
  80. package/src/components/forms/nk-field.js +44 -6
  81. package/src/components/forms/nk-fields.js +21 -0
  82. package/src/components/forms/nk-input.js +1 -1
  83. package/src/components/forms/nk-radio.js +8 -2
  84. package/src/components/forms/nk-select.js +5 -2
  85. package/src/components/forms/nk-switch.js +31 -3
  86. package/src/components/forms/nk-textarea.js +1 -1
  87. package/src/components/shell/nk-tab-bar-item.js +82 -0
  88. package/src/components/shell/nk-tab-bar.js +82 -0
  89. package/src/index.js +3 -0
  90. package/dist/components/shared/base-6gEcoatG.js +0 -317
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungherz-de/notionkit-elements",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
5
5
  "license": "MIT",
6
6
  "author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
@@ -58,10 +58,10 @@
58
58
  "dark-mode"
59
59
  ],
60
60
  "peerDependencies": {
61
- "@jungherz-de/notionkit": ">=1.0.0"
61
+ "@jungherz-de/notionkit": ">=1.3.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@jungherz-de/notionkit": "^1.1.1",
64
+ "@jungherz-de/notionkit": "^1.3.0",
65
65
  "@rollup/plugin-node-resolve": "^15.0.0",
66
66
  "@rollup/plugin-terser": "^0.4.0",
67
67
  "rollup": "^4.0.0",
package/src/base.js CHANGED
@@ -137,6 +137,12 @@ export class NkElement extends HTMLElement {
137
137
 
138
138
  connectedCallback() {
139
139
  if (!this._initialized) {
140
+ // A framework may have set properties on the instance before the
141
+ // definition arrived (hybrids binds checked="${bool}" as a property
142
+ // while the element is still a plain HTMLElement). Such an own property
143
+ // shadows the accessor on the prototype for good; re-apply it through
144
+ // the setter so the attribute – and with it render() – sees the value.
145
+ this._upgradeOwnProperties();
140
146
  this._initialized = true;
141
147
 
142
148
  // The wrapper mirrors data-theme from <html> so [data-theme]-keyed
@@ -187,6 +193,18 @@ export class NkElement extends HTMLElement {
187
193
  this._wrapper?.setAttribute('data-theme', theme);
188
194
  }
189
195
 
196
+ _upgradeOwnProperties() {
197
+ for (const key of Object.keys(this)) {
198
+ if (key.startsWith('_')) continue;
199
+ let proto = Object.getPrototypeOf(this), desc;
200
+ while (proto && proto !== HTMLElement.prototype && !(desc = Object.getOwnPropertyDescriptor(proto, key))) proto = Object.getPrototypeOf(proto);
201
+ if (!desc?.set) continue;
202
+ const value = this[key];
203
+ delete this[key];
204
+ this[key] = value;
205
+ }
206
+ }
207
+
190
208
  /** Subclasses override to build the inner DOM inside this._wrapper. Runs once. */
191
209
  render() {}
192
210
 
@@ -268,9 +286,21 @@ export class NkFormElement extends NkElement {
268
286
  checkValidity() { return this._internals.checkValidity(); }
269
287
  reportValidity() { return this._internals.reportValidity(); }
270
288
 
289
+ connectedCallback() {
290
+ const first = !this._initialized;
291
+ super.connectedCallback();
292
+ // A `disabled` attribute in the markup fires formDisabledCallback on
293
+ // parse or upgrade – before the first connect, when nothing is rendered
294
+ // yet. The state was remembered below; apply it now that render() ran.
295
+ if (first && this._formDisabled) this.onFormDisabled(true);
296
+ }
297
+
271
298
  formResetCallback() { this.resetValue(); }
272
299
  formStateRestoreCallback(state, mode) { this.restoreValue(state, mode); }
273
- formDisabledCallback(disabled) { this.onFormDisabled(disabled); }
300
+ formDisabledCallback(disabled) {
301
+ this._formDisabled = disabled;
302
+ if (this._initialized) this.onFormDisabled(disabled);
303
+ }
274
304
 
275
305
  /** Subclasses override. */
276
306
  resetValue() {}
@@ -3,15 +3,18 @@ import { NkFormElement } from '../../base.js';
3
3
  // <nk-check name="mail" value="weekly" checked>Weekly digest</nk-check>
4
4
  // → <label class="nk-check"><input type="checkbox"> Weekly digest</label>
5
5
  class NkCheck extends NkFormElement {
6
- static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'indeterminate', 'required']; }
6
+ static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'text', 'indeterminate', 'required']; }
7
7
 
8
8
  render() {
9
9
  const label = this.createElement('label', ['nk-check']);
10
10
  this._input = this.createElement('input', [], { type: 'checkbox' });
11
+ const slot = document.createElement('slot');
12
+ this._text = document.createTextNode('');
13
+ slot.appendChild(this._text);
11
14
  this._apply();
12
15
  this._defaultChecked = this.getBoolAttr('checked');
13
16
  label.appendChild(this._input);
14
- label.appendChild(document.createElement('slot'));
17
+ label.appendChild(slot);
15
18
  this._wrapper.appendChild(label);
16
19
  this._syncFormValue();
17
20
  }
@@ -24,6 +27,7 @@ class NkCheck extends NkFormElement {
24
27
  const name = this.getAttribute('name');
25
28
  name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
26
29
  this._input.value = this.getAttribute('value') || 'on';
30
+ if (this._text) this._text.data = this.getAttribute('text') ?? '';
27
31
  }
28
32
 
29
33
  setupEvents() {
@@ -68,6 +72,8 @@ class NkCheck extends NkFormElement {
68
72
  set disabled(v) { this.setBoolAttr('disabled', v); }
69
73
  get name() { return this.getAttribute('name'); }
70
74
  set name(v) { this.setAttribute('name', v); }
75
+ get text() { return this.getAttribute('text'); }
76
+ set text(v) { v == null ? this.removeAttribute('text') : this.setAttribute('text', v); }
71
77
  get value() { return this.getAttribute('value') || 'on'; }
72
78
  set value(v) { this.setAttribute('value', v); }
73
79
  }
@@ -7,11 +7,19 @@ import { NkElement } from '../../base.js';
7
7
  // <div class="f-control">…</div></div>
8
8
  // Rich label / description content goes through slot="label" / slot="desc"
9
9
  // (twins `.nk-field ::slotted([slot="label"])` etc. in notionkit.css).
10
+ //
11
+ // `stacked` puts the label above a full-width control, `compact` shrinks the
12
+ // label and drops the row padding; inside <nk-fields> both are on by
13
+ // default. A stacked field sets `wide` on its nk-input / nk-textarea /
14
+ // nk-select, since the stylesheet's `.nk-field.stacked .nk-input` cannot
15
+ // reach into the control's shadow root.
16
+ const WIDE = ['nk-input', 'nk-textarea', 'nk-select'];
17
+
10
18
  class NkField extends NkElement {
11
- static get observedAttributes() { return ['label', 'desc']; }
19
+ static get observedAttributes() { return ['label', 'desc', 'stacked', 'compact']; }
12
20
 
13
21
  render() {
14
- const field = this.createElement('div', ['nk-field']);
22
+ this._field = this.createElement('div', ['nk-field']);
15
23
  const text = document.createElement('div');
16
24
  this._label = this.createElement('div', ['f-label']);
17
25
  const labelSlot = this.createElement('slot', [], { name: 'label' });
@@ -22,21 +30,51 @@ class NkField extends NkElement {
22
30
  text.appendChild(labelSlot);
23
31
  text.appendChild(descSlot);
24
32
  const control = this.createElement('div', ['f-control']);
25
- control.appendChild(document.createElement('slot'));
26
- field.appendChild(text);
27
- field.appendChild(control);
28
- this._wrapper.appendChild(field);
33
+ this._slot = document.createElement('slot');
34
+ control.appendChild(this._slot);
35
+ this._field.appendChild(text);
36
+ this._field.appendChild(control);
37
+ this._wrapper.appendChild(this._field);
38
+ this._widened = new Set();
29
39
  this._sync();
30
40
  }
31
41
 
42
+ setupEvents() {
43
+ this._onSlot = () => this._syncWide();
44
+ this._slot.addEventListener('slotchange', this._onSlot);
45
+ this._sync();
46
+ }
47
+
48
+ teardownEvents() {
49
+ this._slot?.removeEventListener('slotchange', this._onSlot);
50
+ }
51
+
32
52
  onAttributeChanged() { this._sync(); }
33
53
 
54
+ get stacked() { return this.getBoolAttr('stacked') || !!this.closest('nk-fields'); }
55
+ set stacked(v) { this.setBoolAttr('stacked', v); }
56
+ get compact() { return this.getBoolAttr('compact') || !!this.closest('nk-fields'); }
57
+ set compact(v) { this.setBoolAttr('compact', v); }
58
+
34
59
  _sync() {
35
60
  if (!this._label) return;
36
61
  this._label.textContent = this.getAttribute('label') || '';
37
62
  const desc = this.getAttribute('desc');
38
63
  this._desc.textContent = desc || '';
39
64
  this._desc.style.display = desc ? '' : 'none';
65
+ this._field.classList.toggle('stacked', this.stacked);
66
+ this._field.classList.toggle('compact', this.compact);
67
+ this._syncWide();
68
+ }
69
+
70
+ _syncWide() {
71
+ if (!this._slot) return;
72
+ const stacked = this.stacked;
73
+ for (const el of this._slot.assignedElements()) {
74
+ if (!WIDE.includes(el.localName)) continue;
75
+ if (stacked && !el.hasAttribute('wide')) { el.setAttribute('wide', ''); this._widened.add(el); }
76
+ else if (!stacked && this._widened.has(el)) { el.removeAttribute('wide'); this._widened.delete(el); }
77
+ }
40
78
  }
41
79
 
42
80
  get label() { return this.getAttribute('label'); }
@@ -0,0 +1,21 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-fields>
4
+ // <nk-field label="Name"><nk-input name="name"></nk-input></nk-field>
5
+ // <nk-field label="Email"><nk-input name="email"></nk-input></nk-field>
6
+ // </nk-fields>
7
+ // → <div class="nk-fields">…</div>
8
+ // A grid of short fields side by side, wrapping as the width allows. Every
9
+ // nk-field inside renders itself stacked and compact (12px label above a
10
+ // full-width control) – the class markup does the same through
11
+ // `.nk-fields > .nk-field`, which cannot cross the shadow boundary here.
12
+ class NkFields extends NkElement {
13
+ render() {
14
+ const grid = this.createElement('div', ['nk-fields']);
15
+ grid.appendChild(document.createElement('slot'));
16
+ this._wrapper.appendChild(grid);
17
+ }
18
+ }
19
+
20
+ customElements.define('nk-fields', NkFields);
21
+ export { NkFields };
@@ -21,7 +21,7 @@ class NkInput extends NkFormElement {
21
21
 
22
22
  _forward(attr) {
23
23
  if (BOOLEANS.includes(attr)) {
24
- this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
24
+ this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr) || (attr === 'disabled' && !!this._formDisabled);
25
25
  return;
26
26
  }
27
27
  const v = this.getAttribute(attr);
@@ -29,17 +29,20 @@ function syncGroupTabIndex(group) {
29
29
  }
30
30
 
31
31
  class NkRadio extends NkFormElement {
32
- static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'required']; }
32
+ static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'text', 'required']; }
33
33
 
34
34
  render() {
35
35
  const label = this.createElement('label', ['nk-check']);
36
36
  this._input = this.createElement('input', [], { type: 'radio' });
37
+ const slot = document.createElement('slot');
38
+ this._text = document.createTextNode('');
39
+ slot.appendChild(this._text);
37
40
  // `name` on the inner input is inert (it is alone in its tree); it stays
38
41
  // as a devtools courtesy. The group above is the real one.
39
42
  this._apply();
40
43
  this._defaultChecked = this.getBoolAttr('checked');
41
44
  label.appendChild(this._input);
42
- label.appendChild(document.createElement('slot'));
45
+ label.appendChild(slot);
43
46
  this._wrapper.appendChild(label);
44
47
  this._syncFormValue();
45
48
  // Elements upgrade in document order, so the last `checked` one in the
@@ -55,6 +58,7 @@ class NkRadio extends NkFormElement {
55
58
  const name = this.getAttribute('name');
56
59
  name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
57
60
  this._input.value = this.getAttribute('value') || '';
61
+ if (this._text) this._text.data = this.getAttribute('text') ?? '';
58
62
  }
59
63
 
60
64
  setupEvents() {
@@ -151,6 +155,8 @@ class NkRadio extends NkFormElement {
151
155
  set disabled(v) { this.setBoolAttr('disabled', v); }
152
156
  get name() { return this.getAttribute('name'); }
153
157
  set name(v) { this.setAttribute('name', v); }
158
+ get text() { return this.getAttribute('text'); }
159
+ set text(v) { v == null ? this.removeAttribute('text') : this.setAttribute('text', v); }
154
160
  get value() { return this.getAttribute('value') || ''; }
155
161
  set value(v) { this.setAttribute('value', v); }
156
162
  }
@@ -9,7 +9,7 @@ import { NkFormElement } from '../../base.js';
9
9
  // MutationObserver keeps the copy in step when a framework swaps them; the
10
10
  // empty string is a valid value; the live selection survives a rebuild.
11
11
  class NkSelect extends NkFormElement {
12
- static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'aria-label']; }
12
+ static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'wide', 'aria-label']; }
13
13
  static get observesLightDom() { return true; }
14
14
 
15
15
  render() {
@@ -27,9 +27,10 @@ class NkSelect extends NkFormElement {
27
27
  name ? this._select.setAttribute('name', name) : this._select.removeAttribute('name');
28
28
  const label = this.getAttribute('aria-label');
29
29
  label ? this._select.setAttribute('aria-label', label) : this._select.removeAttribute('aria-label');
30
- this._select.disabled = this.getBoolAttr('disabled');
30
+ this._select.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
31
31
  this._select.required = this.getBoolAttr('required');
32
32
  this._select.classList.toggle('compact', this.getBoolAttr('compact'));
33
+ this._select.classList.toggle('wide', this.getBoolAttr('wide'));
33
34
  }
34
35
 
35
36
  projectLightDom() {
@@ -120,6 +121,8 @@ class NkSelect extends NkFormElement {
120
121
  set required(v) { this.setBoolAttr('required', v); }
121
122
  get compact() { return this.getBoolAttr('compact'); }
122
123
  set compact(v) { this.setBoolAttr('compact', v); }
124
+ get wide() { return this.getBoolAttr('wide'); }
125
+ set wide(v) { this.setBoolAttr('wide', v); }
123
126
  }
124
127
 
125
128
  customElements.define('nk-select', NkSelect);
@@ -3,14 +3,28 @@ import { NkFormElement } from '../../base.js';
3
3
  // <nk-switch name="notify" checked></nk-switch>
4
4
  // → <button class="nk-switch" role="switch" aria-checked="true">
5
5
  // notionkit.css styles the state through [aria-checked]; the toggling is here.
6
+ //
7
+ // <nk-switch checked>Waiting</nk-switch> (or text="Waiting")
8
+ // → <label class="nk-switch-label"><button class="nk-switch" …></button><span>Waiting</span></label>
9
+ // Visible text beside the switch, for several switches in one row where no
10
+ // nk-field row names them. Without text the label wrapper is layout-
11
+ // transparent (display: contents), so the bare button stays the bare button.
6
12
  class NkSwitch extends NkFormElement {
7
- static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'label']; }
13
+ static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'label', 'text']; }
8
14
 
9
15
  render() {
16
+ this._row = this.createElement('label', ['nk-switch-label']);
10
17
  this._btn = this.createElement('button', ['nk-switch'], { type: 'button', role: 'switch' });
18
+ this._text = document.createElement('span');
19
+ this._slot = document.createElement('slot');
20
+ this._textNode = document.createTextNode('');
21
+ this._slot.appendChild(this._textNode);
22
+ this._text.appendChild(this._slot);
23
+ this._row.append(this._btn, this._text);
11
24
  this._defaultChecked = this.getBoolAttr('checked');
12
25
  this._apply();
13
- this._wrapper.appendChild(this._btn);
26
+ this._syncText();
27
+ this._wrapper.appendChild(this._row);
14
28
  this._syncFormValue();
15
29
  }
16
30
 
@@ -21,13 +35,24 @@ class NkSwitch extends NkFormElement {
21
35
  label ? this._btn.setAttribute('aria-label', label) : this._btn.removeAttribute('aria-label');
22
36
  }
23
37
 
38
+ _syncText() {
39
+ this._textNode.data = this.getAttribute('text') ?? '';
40
+ const hasText = !!(this.getAttribute('text') || this._slot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.data.trim()));
41
+ this._text.hidden = !hasText;
42
+ this._row.style.display = hasText ? '' : 'contents';
43
+ }
44
+
24
45
  setupEvents() {
25
46
  this._onClick = () => this.toggle();
47
+ this._onSlot = () => this._syncText();
26
48
  this._btn.addEventListener('click', this._onClick);
49
+ this._slot.addEventListener('slotchange', this._onSlot);
50
+ this._syncText();
27
51
  }
28
52
 
29
53
  teardownEvents() {
30
54
  this._btn?.removeEventListener('click', this._onClick);
55
+ this._slot?.removeEventListener('slotchange', this._onSlot);
31
56
  }
32
57
 
33
58
  toggle() {
@@ -37,8 +62,9 @@ class NkSwitch extends NkFormElement {
37
62
  this.dispatchEvent(new Event('change', { bubbles: true }));
38
63
  }
39
64
 
40
- onAttributeChanged() {
65
+ onAttributeChanged(name) {
41
66
  if (!this._btn) return;
67
+ if (name === 'text') { this._syncText(); return; }
42
68
  this._apply();
43
69
  this._syncFormValue();
44
70
  }
@@ -62,6 +88,8 @@ class NkSwitch extends NkFormElement {
62
88
  set name(v) { this.setAttribute('name', v); }
63
89
  get value() { return this.getAttribute('value') || 'on'; }
64
90
  set value(v) { this.setAttribute('value', v); }
91
+ get text() { return this.getAttribute('text'); }
92
+ set text(v) { v == null ? this.removeAttribute('text') : this.setAttribute('text', v); }
65
93
  }
66
94
 
67
95
  customElements.define('nk-switch', NkSwitch);
@@ -19,7 +19,7 @@ class NkTextarea extends NkFormElement {
19
19
 
20
20
  _forward(attr) {
21
21
  if (BOOLEANS.includes(attr)) {
22
- this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
22
+ this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr) || (attr === 'disabled' && !!this._formDisabled);
23
23
  return;
24
24
  }
25
25
  const v = this.getAttribute(attr);
@@ -0,0 +1,82 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tab-bar-item icon="📥" value="inbox" active>Inbox</nk-tab-bar-item>
4
+ // → <button class="nk-tab-bar-item active" type="button">
5
+ // <span class="icon">📥</span><span class="label">Inbox</span></button>
6
+ //
7
+ // One destination of <nk-tab-bar>. A click emits nk-select (cancelable);
8
+ // inside a bar it then moves the bar's `value`, standalone it sets its own
9
+ // `active`. With `href` it navigates afterwards, with `drawer` it opens the
10
+ // nearest <nk-sidebar> as a drawer instead of becoming active – the usual
11
+ // fifth item ("More"). The icon is an attribute or a `slot="icon"` node.
12
+ class NkTabBarItem extends NkElement {
13
+ static get observedAttributes() { return ['icon', 'label', 'value', 'href', 'active', 'disabled', 'drawer']; }
14
+
15
+ render() {
16
+ this._btn = this.createElement('button', ['nk-tab-bar-item'], { type: 'button' });
17
+ this._iconSlot = this.createElement('slot', [], { name: 'icon' });
18
+ this._icon = this.createElement('span', ['icon'], { 'aria-hidden': 'true' });
19
+ this._iconSlot.appendChild(this._icon);
20
+ this._label = this.createElement('span', ['label']);
21
+ this._labelSlot = document.createElement('slot');
22
+ this._labelText = document.createTextNode('');
23
+ this._labelSlot.appendChild(this._labelText);
24
+ this._label.appendChild(this._labelSlot);
25
+ this._btn.append(this._iconSlot, this._label);
26
+ this._wrapper.appendChild(this._btn);
27
+ this._sync();
28
+ }
29
+
30
+ _sync() {
31
+ this._icon.textContent = this.getAttribute('icon') ?? '';
32
+ this._labelText.data = this.getAttribute('label') ?? '';
33
+ const active = this.getBoolAttr('active');
34
+ this._btn.classList.toggle('active', active);
35
+ active ? this._btn.setAttribute('aria-current', 'page') : this._btn.removeAttribute('aria-current');
36
+ this._btn.disabled = this.getBoolAttr('disabled');
37
+ const href = this.getAttribute('href');
38
+ href ? this._btn.setAttribute('data-href', href) : this._btn.removeAttribute('data-href');
39
+ }
40
+
41
+ setupEvents() {
42
+ this._onClick = () => this.select();
43
+ this._btn.addEventListener('click', this._onClick);
44
+ }
45
+
46
+ teardownEvents() {
47
+ this._btn?.removeEventListener('click', this._onClick);
48
+ }
49
+
50
+ select() {
51
+ if (this.getBoolAttr('disabled')) return;
52
+ const href = this.getAttribute('href');
53
+ const ok = this.emit('nk-select', { value: this.value, label: this.label, href, item: this, drawer: this.getBoolAttr('drawer') });
54
+ if (!ok) return;
55
+ if (this.getBoolAttr('drawer')) {
56
+ const sidebar = this.closest('nk-app')?.querySelector('nk-sidebar') ?? document.querySelector('nk-sidebar');
57
+ sidebar?.toggle?.();
58
+ return;
59
+ }
60
+ const bar = this.closest('nk-tab-bar');
61
+ if (bar) bar.value = this.value;
62
+ else this.setBoolAttr('active', true);
63
+ if (href) location.assign(href);
64
+ }
65
+
66
+ onAttributeChanged() { this._sync(); }
67
+ focus(o) { this._btn?.focus(o); }
68
+
69
+ get value() { return this.getAttribute('value') ?? this.label; }
70
+ set value(v) { this.setAttribute('value', v); }
71
+ get label() { return this.getAttribute('label') ?? this.textContent.trim(); }
72
+ set label(v) { this.setAttribute('label', v); }
73
+ get active() { return this.getBoolAttr('active'); }
74
+ set active(v) { this.setBoolAttr('active', v); }
75
+ get disabled() { return this.getBoolAttr('disabled'); }
76
+ set disabled(v) { this.setBoolAttr('disabled', v); }
77
+ get drawer() { return this.getBoolAttr('drawer'); }
78
+ set drawer(v) { this.setBoolAttr('drawer', v); }
79
+ }
80
+
81
+ customElements.define('nk-tab-bar-item', NkTabBarItem);
82
+ export { NkTabBarItem };
@@ -0,0 +1,82 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-tab-bar value="inbox">
4
+ // <nk-tab-bar-item icon="🏠" value="home">Home</nk-tab-bar-item>
5
+ // <nk-tab-bar-item icon="📥" value="inbox">Inbox</nk-tab-bar-item>
6
+ // <nk-tab-bar-item icon="☰" drawer>More</nk-tab-bar-item>
7
+ // </nk-tab-bar>
8
+ // → <nav class="nk-tab-bar">…</nav>
9
+ //
10
+ // The thumb-reachable twin of the sidebar for phones and installed PWAs. As
11
+ // the last child of <nk-app> it is slotted into .nk-main below the scrolling
12
+ // page, so it never moves and needs no bottom padding anywhere. The
13
+ // stylesheet hides it above 860px – there the sidebar is the navigation –
14
+ // and shows it below; `always` shows it at every width (previews, phone
15
+ // frames), `floating` makes it a capsule. Exactly one item is `active`; a
16
+ // `drawer` item opens the sidebar instead of becoming active.
17
+ class NkTabBar extends NkElement {
18
+ static get observedAttributes() { return ['value', 'always', 'floating', 'label']; }
19
+
20
+ render() {
21
+ this._bar = this.createElement('nav', ['nk-tab-bar']);
22
+ this._slot = document.createElement('slot');
23
+ this._bar.appendChild(this._slot);
24
+ this._wrapper.appendChild(this._bar);
25
+ this._syncBar();
26
+ this._sync();
27
+ }
28
+
29
+ get items() { return [...this.querySelectorAll(':scope > nk-tab-bar-item')]; }
30
+
31
+ // Attribute access only: on the first connect the children are in the DOM
32
+ // but not yet upgraded, so their accessors do not exist.
33
+ _valueOf(item) { return item.getAttribute('value') ?? item.getAttribute('label') ?? item.textContent.trim(); }
34
+
35
+ _syncBar() {
36
+ this._bar.classList.toggle('always', this.getBoolAttr('always'));
37
+ this._bar.classList.toggle('floating', this.getBoolAttr('floating'));
38
+ const label = this.getAttribute('label');
39
+ label ? this._bar.setAttribute('aria-label', label) : this._bar.removeAttribute('aria-label');
40
+ }
41
+
42
+ _sync() {
43
+ const items = this.items.filter(i => !i.hasAttribute('drawer'));
44
+ if (!items.length) return;
45
+ let value = this.getAttribute('value');
46
+ if (value === null || !items.some(i => this._valueOf(i) === value)) {
47
+ const preset = items.find(i => i.hasAttribute('active')) || items[0];
48
+ value = this._valueOf(preset);
49
+ }
50
+ for (const item of items) {
51
+ if (this._valueOf(item) === value) item.setAttribute('active', ''); else item.removeAttribute('active');
52
+ }
53
+ this._value = value;
54
+ }
55
+
56
+ setupEvents() {
57
+ this._onSlot = () => this._sync();
58
+ this._slot.addEventListener('slotchange', this._onSlot);
59
+ this._sync();
60
+ }
61
+
62
+ teardownEvents() {
63
+ this._slot?.removeEventListener('slotchange', this._onSlot);
64
+ }
65
+
66
+ onAttributeChanged(name) {
67
+ if (name !== 'value') { this._syncBar(); return; }
68
+ const before = this._value;
69
+ this._sync();
70
+ if (this._value !== before) this.emit('nk-change', { value: this._value });
71
+ }
72
+
73
+ get value() { return this._value ?? this.getAttribute('value'); }
74
+ set value(v) { this.setAttribute('value', v); }
75
+ get always() { return this.getBoolAttr('always'); }
76
+ set always(v) { this.setBoolAttr('always', v); }
77
+ get floating() { return this.getBoolAttr('floating'); }
78
+ set floating(v) { this.setBoolAttr('floating', v); }
79
+ }
80
+
81
+ customElements.define('nk-tab-bar', NkTabBar);
82
+ export { NkTabBar };
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export { NkCheck } from './components/forms/nk-check.js';
16
16
  export { NkRadio } from './components/forms/nk-radio.js';
17
17
  export { NkSlider } from './components/forms/nk-slider.js';
18
18
  export { NkField } from './components/forms/nk-field.js';
19
+ export { NkFields } from './components/forms/nk-fields.js';
19
20
 
20
21
  // Wave 1 – content
21
22
  export { NkTag } from './components/content/nk-tag.js';
@@ -39,6 +40,8 @@ export { NkTreeItem } from './components/shell/nk-tree-item.js';
39
40
  export { NkTopbar } from './components/shell/nk-topbar.js';
40
41
  export { NkBreadcrumb } from './components/shell/nk-breadcrumb.js';
41
42
  export { NkThemeToggle } from './components/shell/nk-theme-toggle.js';
43
+ export { NkTabBar } from './components/shell/nk-tab-bar.js';
44
+ export { NkTabBarItem } from './components/shell/nk-tab-bar-item.js';
42
45
 
43
46
  // Wave 3 – page shell & blocks
44
47
  export { NkPage } from './components/page/nk-page.js';