@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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/components/nk-ai-input-row.js +57 -0
- package/dist/components/nk-ai-msg.js +67 -0
- package/dist/components/nk-ai-thread.js +14 -0
- package/dist/components/nk-app.js +23 -0
- package/dist/components/nk-avatar-group.js +35 -0
- package/dist/components/nk-banner.js +30 -0
- package/dist/components/nk-block-host.js +35 -0
- package/dist/components/nk-board-view.js +129 -0
- package/dist/components/nk-breadcrumb.js +78 -0
- package/dist/components/nk-btn.js +107 -0
- package/dist/components/nk-callout.js +36 -0
- package/dist/components/nk-check.js +77 -0
- package/dist/components/nk-cmdk.js +221 -0
- package/dist/components/nk-code.js +66 -0
- package/dist/components/nk-comment.js +41 -0
- package/dist/components/nk-comments.js +57 -0
- package/dist/components/nk-danger-zone.js +27 -0
- package/dist/components/nk-database.js +100 -0
- package/dist/components/nk-divider.js +12 -0
- package/dist/components/nk-emoji-picker.js +112 -0
- package/dist/components/nk-empty.js +36 -0
- package/dist/components/nk-field.js +50 -0
- package/dist/components/nk-filter-bar.js +78 -0
- package/dist/components/nk-heading.js +29 -0
- package/dist/components/nk-input.js +108 -0
- package/dist/components/nk-kbd.js +15 -0
- package/dist/components/nk-member-list.js +30 -0
- package/dist/components/nk-member-row.js +46 -0
- package/dist/components/nk-mention.js +31 -0
- package/dist/components/nk-menu-item.js +68 -0
- package/dist/components/nk-menu.js +45 -0
- package/dist/components/nk-modal.js +166 -0
- package/dist/components/nk-model-card.js +89 -0
- package/dist/components/nk-page-actions.js +15 -0
- package/dist/components/nk-page-cover.js +35 -0
- package/dist/components/nk-page-title.js +71 -0
- package/dist/components/nk-page.js +71 -0
- package/dist/components/nk-pop.js +90 -0
- package/dist/components/nk-profile-row.js +25 -0
- package/dist/components/nk-progress.js +43 -0
- package/dist/components/nk-quote.js +32 -0
- package/dist/components/nk-radio.js +160 -0
- package/dist/components/nk-section-label.js +42 -0
- package/dist/components/nk-segmented.js +99 -0
- package/dist/components/nk-select.js +127 -0
- package/dist/components/nk-settings-pane.js +40 -0
- package/dist/components/nk-settings-user.js +36 -0
- package/dist/components/nk-sidebar.js +81 -0
- package/dist/components/nk-skeleton.js +31 -0
- package/dist/components/nk-slider.js +85 -0
- package/dist/components/nk-stat.js +40 -0
- package/dist/components/nk-stats.js +14 -0
- package/dist/components/nk-switch.js +69 -0
- package/dist/components/nk-synced.js +26 -0
- package/dist/components/nk-tab.js +54 -0
- package/dist/components/nk-table-view.js +109 -0
- package/dist/components/nk-tabs.js +82 -0
- package/dist/components/nk-tag.js +32 -0
- package/dist/components/nk-template-btn.js +43 -0
- package/dist/components/nk-textarea.js +95 -0
- package/dist/components/nk-theme-toggle.js +67 -0
- package/dist/components/nk-toast.js +51 -0
- package/dist/components/nk-todo.js +73 -0
- package/dist/components/nk-toggle.js +56 -0
- package/dist/components/nk-topbar.js +24 -0
- package/dist/components/nk-tree-item.js +155 -0
- package/dist/components/nk-tree.js +123 -0
- package/dist/components/nk-workspace-switcher.js +82 -0
- package/dist/components/shared/base-6gEcoatG.js +317 -0
- package/dist/components/shared/focus-DXrK9oLe.js +47 -0
- package/dist/components/shared/property-cell-Dn91uFkf.js +92 -0
- package/dist/notionkit-elements.esm.js +4639 -0
- package/dist/notionkit-elements.js +4718 -0
- package/dist/notionkit-elements.min.js +1 -0
- package/package.json +72 -0
- package/src/base.js +296 -0
- package/src/components/content/nk-callout.js +35 -0
- package/src/components/content/nk-code.js +51 -0
- package/src/components/content/nk-divider.js +11 -0
- package/src/components/content/nk-heading.js +28 -0
- package/src/components/content/nk-kbd.js +14 -0
- package/src/components/content/nk-progress.js +42 -0
- package/src/components/content/nk-quote.js +31 -0
- package/src/components/content/nk-tag.js +31 -0
- package/src/components/content/nk-todo.js +72 -0
- package/src/components/content/nk-toggle.js +55 -0
- package/src/components/data/nk-ai-input-row.js +56 -0
- package/src/components/data/nk-ai-msg.js +66 -0
- package/src/components/data/nk-ai-thread.js +13 -0
- package/src/components/data/nk-board-view.js +128 -0
- package/src/components/data/nk-comment.js +40 -0
- package/src/components/data/nk-comments.js +56 -0
- package/src/components/data/nk-database.js +99 -0
- package/src/components/data/nk-filter-bar.js +77 -0
- package/src/components/data/nk-table-view.js +108 -0
- package/src/components/forms/nk-btn.js +106 -0
- package/src/components/forms/nk-check.js +76 -0
- package/src/components/forms/nk-field.js +49 -0
- package/src/components/forms/nk-input.js +107 -0
- package/src/components/forms/nk-radio.js +159 -0
- package/src/components/forms/nk-select.js +126 -0
- package/src/components/forms/nk-slider.js +84 -0
- package/src/components/forms/nk-switch.js +68 -0
- package/src/components/forms/nk-textarea.js +94 -0
- package/src/components/overlays/nk-cmdk.js +201 -0
- package/src/components/overlays/nk-emoji-picker.js +94 -0
- package/src/components/overlays/nk-menu-item.js +67 -0
- package/src/components/overlays/nk-menu.js +44 -0
- package/src/components/overlays/nk-modal.js +165 -0
- package/src/components/overlays/nk-pop.js +89 -0
- package/src/components/overlays/nk-settings-pane.js +39 -0
- package/src/components/overlays/nk-settings-user.js +35 -0
- package/src/components/overlays/nk-toast.js +50 -0
- package/src/components/page/nk-avatar-group.js +34 -0
- package/src/components/page/nk-banner.js +29 -0
- package/src/components/page/nk-block-host.js +34 -0
- package/src/components/page/nk-danger-zone.js +26 -0
- package/src/components/page/nk-empty.js +35 -0
- package/src/components/page/nk-member-list.js +29 -0
- package/src/components/page/nk-member-row.js +45 -0
- package/src/components/page/nk-mention.js +30 -0
- package/src/components/page/nk-model-card.js +88 -0
- package/src/components/page/nk-page-actions.js +14 -0
- package/src/components/page/nk-page-cover.js +34 -0
- package/src/components/page/nk-page-title.js +70 -0
- package/src/components/page/nk-page.js +70 -0
- package/src/components/page/nk-profile-row.js +24 -0
- package/src/components/page/nk-segmented.js +98 -0
- package/src/components/page/nk-skeleton.js +30 -0
- package/src/components/page/nk-stat.js +39 -0
- package/src/components/page/nk-stats.js +13 -0
- package/src/components/page/nk-synced.js +25 -0
- package/src/components/page/nk-tab.js +53 -0
- package/src/components/page/nk-tabs.js +81 -0
- package/src/components/page/nk-template-btn.js +42 -0
- package/src/components/shell/nk-app.js +22 -0
- package/src/components/shell/nk-breadcrumb.js +77 -0
- package/src/components/shell/nk-section-label.js +41 -0
- package/src/components/shell/nk-sidebar.js +80 -0
- package/src/components/shell/nk-theme-toggle.js +66 -0
- package/src/components/shell/nk-topbar.js +23 -0
- package/src/components/shell/nk-tree-item.js +154 -0
- package/src/components/shell/nk-tree.js +122 -0
- package/src/components/shell/nk-workspace-switcher.js +81 -0
- package/src/index.js +88 -0
- package/src/util/emoji.js +17 -0
- package/src/util/focus.js +45 -0
- package/src/util/fuzzy.js +19 -0
- package/src/util/highlight.js +14 -0
- package/src/util/property-cell.js +90 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-pop placement="bottom-start">
|
|
4
|
+
// <nk-btn slot="trigger" variant="secondary">Options</nk-btn>
|
|
5
|
+
// <nk-menu>…</nk-menu>
|
|
6
|
+
// </nk-pop>
|
|
7
|
+
// The trigger stays in the light DOM; a click on it toggles `open`, outside
|
|
8
|
+
// clicks and Escape close. Content is wrapped in `.nk-pop` unless it brings
|
|
9
|
+
// its own surface (nk-menu, nk-emoji-picker) or `bare` is set. The only
|
|
10
|
+
// styling here is positioning – the surface look is the stylesheet's.
|
|
11
|
+
const popSheet = new CSSStyleSheet();
|
|
12
|
+
popSheet.replaceSync(`
|
|
13
|
+
.nk-pop-anchor { position: relative; display: inline-block; }
|
|
14
|
+
.nk-pop-float { display: none; position: absolute; z-index: 50; top: 100%; left: 0; margin-top: 4px; }
|
|
15
|
+
:host([open]) .nk-pop-float { display: block; }
|
|
16
|
+
:host([placement="bottom-end"]) .nk-pop-float { left: auto; right: 0; }
|
|
17
|
+
:host([placement="top-start"]) .nk-pop-float { top: auto; bottom: 100%; margin: 0 0 4px; }
|
|
18
|
+
:host([placement="top-end"]) .nk-pop-float { top: auto; bottom: 100%; left: auto; right: 0; margin: 0 0 4px; }
|
|
19
|
+
`);
|
|
20
|
+
const OWN_SURFACE = new Set(['nk-menu', 'nk-emoji-picker']);
|
|
21
|
+
|
|
22
|
+
class NkPop extends NkElement {
|
|
23
|
+
static get hostStyles() { return popSheet; }
|
|
24
|
+
static get observedAttributes() { return ['open', 'placement', 'bare']; }
|
|
25
|
+
|
|
26
|
+
render() {
|
|
27
|
+
const anchor = this.createElement('div', ['nk-pop-anchor']);
|
|
28
|
+
this._triggerSlot = this.createElement('slot', [], { name: 'trigger' });
|
|
29
|
+
this._float = this.createElement('div', ['nk-pop-float']);
|
|
30
|
+
this._surface = this.createElement('div', ['nk-pop']);
|
|
31
|
+
this._slot = document.createElement('slot');
|
|
32
|
+
this._surface.appendChild(this._slot);
|
|
33
|
+
this._float.appendChild(this._surface);
|
|
34
|
+
anchor.append(this._triggerSlot, this._float);
|
|
35
|
+
this._wrapper.appendChild(anchor);
|
|
36
|
+
this._syncSurface();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_syncSurface() {
|
|
40
|
+
const bare = this.getBoolAttr('bare') || this._slot.assignedElements().some(el => OWN_SURFACE.has(el.localName));
|
|
41
|
+
this._surface.className = bare ? '' : 'nk-pop';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setupEvents() {
|
|
45
|
+
this._onTrigger = (e) => {
|
|
46
|
+
if (!e.composedPath().some(n => n instanceof Element && n.getAttribute?.('slot') === 'trigger')) return;
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
this.toggle();
|
|
49
|
+
};
|
|
50
|
+
this._onDocClick = (e) => { if (this.getBoolAttr('open') && !e.composedPath().includes(this)) this.close(); };
|
|
51
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
52
|
+
this._onSelect = (e) => { if (e.target !== this) this.close(); };
|
|
53
|
+
this._onSlot = () => this._syncSurface();
|
|
54
|
+
this.addEventListener('click', this._onTrigger);
|
|
55
|
+
document.addEventListener('click', this._onDocClick);
|
|
56
|
+
document.addEventListener('keydown', this._onKey);
|
|
57
|
+
this._float.addEventListener('nk-select', this._onSelect);
|
|
58
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
teardownEvents() {
|
|
62
|
+
this.removeEventListener('click', this._onTrigger);
|
|
63
|
+
document.removeEventListener('click', this._onDocClick);
|
|
64
|
+
document.removeEventListener('keydown', this._onKey);
|
|
65
|
+
this._float?.removeEventListener('nk-select', this._onSelect);
|
|
66
|
+
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onAttributeChanged(name) {
|
|
70
|
+
if (name === 'bare') this._syncSurface();
|
|
71
|
+
if (name === 'open') {
|
|
72
|
+
const open = this.getBoolAttr('open');
|
|
73
|
+
for (const el of this._triggerSlot.assignedElements()) el.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
74
|
+
this.emit('nk-toggle', { open });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
show() { this.setBoolAttr('open', true); }
|
|
79
|
+
close() { this.setBoolAttr('open', false); }
|
|
80
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
81
|
+
|
|
82
|
+
get open() { return this.getBoolAttr('open'); }
|
|
83
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
84
|
+
get placement() { return this.getAttribute('placement') || 'bottom-start'; }
|
|
85
|
+
set placement(v) { this.setAttribute('placement', v); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
customElements.define('nk-pop', NkPop);
|
|
89
|
+
|
|
90
|
+
export { NkPop };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.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
|
+
|
|
25
|
+
export { NkProfileRow };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-progress value="72" label="72%"></nk-progress>
|
|
4
|
+
// The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
|
|
5
|
+
class NkProgress extends NkElement {
|
|
6
|
+
static get observedAttributes() { return ['value', 'max', 'label']; }
|
|
7
|
+
|
|
8
|
+
render() {
|
|
9
|
+
this._bar = this.createElement('span', ['nk-progress'], { role: 'progressbar' });
|
|
10
|
+
this._fill = document.createElement('i');
|
|
11
|
+
this._bar.appendChild(this._fill);
|
|
12
|
+
this._label = this.createElement('span', ['nk-progress-label']);
|
|
13
|
+
this._wrapper.appendChild(this._bar);
|
|
14
|
+
this._wrapper.appendChild(this._label);
|
|
15
|
+
this._sync();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onAttributeChanged() { this._sync(); }
|
|
19
|
+
|
|
20
|
+
_sync() {
|
|
21
|
+
if (!this._bar) return;
|
|
22
|
+
const max = Number(this.getAttribute('max')) || 100;
|
|
23
|
+
const value = Math.min(max, Math.max(0, Number(this.getAttribute('value')) || 0));
|
|
24
|
+
this._fill.style.width = `${(value / max) * 100}%`;
|
|
25
|
+
this._bar.setAttribute('aria-valuenow', value);
|
|
26
|
+
this._bar.setAttribute('aria-valuemin', 0);
|
|
27
|
+
this._bar.setAttribute('aria-valuemax', max);
|
|
28
|
+
const label = this.getAttribute('label');
|
|
29
|
+
this._label.textContent = label ?? '';
|
|
30
|
+
this._label.style.display = label === null ? 'none' : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get value() { return Number(this.getAttribute('value')) || 0; }
|
|
34
|
+
set value(v) { this.setAttribute('value', v); }
|
|
35
|
+
get max() { return Number(this.getAttribute('max')) || 100; }
|
|
36
|
+
set max(v) { this.setAttribute('max', v); }
|
|
37
|
+
get label() { return this.getAttribute('label'); }
|
|
38
|
+
set label(v) { v == null ? this.removeAttribute('label') : this.setAttribute('label', v); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
customElements.define('nk-progress', NkProgress);
|
|
42
|
+
|
|
43
|
+
export { NkProgress };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-quote cite="Ada Lovelace">The Analytical Engine weaves…</nk-quote>
|
|
4
|
+
// .q-cite has no ::slotted() twin, so the citation is rendered here.
|
|
5
|
+
class NkQuote extends NkElement {
|
|
6
|
+
static get observedAttributes() { return ['cite']; }
|
|
7
|
+
|
|
8
|
+
render() {
|
|
9
|
+
this._quote = this.createElement('blockquote', ['nk-quote']);
|
|
10
|
+
this._quote.appendChild(document.createElement('slot'));
|
|
11
|
+
this._cite = this.createElement('cite', ['q-cite']);
|
|
12
|
+
this._quote.appendChild(this._cite);
|
|
13
|
+
this._wrapper.appendChild(this._quote);
|
|
14
|
+
this._sync();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onAttributeChanged() { this._sync(); }
|
|
18
|
+
|
|
19
|
+
_sync() {
|
|
20
|
+
if (!this._cite) return;
|
|
21
|
+
const cite = this.getAttribute('cite');
|
|
22
|
+
this._cite.textContent = cite || '';
|
|
23
|
+
this._cite.style.display = cite ? '' : 'none';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get cite() { return this.getAttribute('cite'); }
|
|
27
|
+
set cite(v) { v == null ? this.removeAttribute('cite') : this.setAttribute('cite', v); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
customElements.define('nk-quote', NkQuote);
|
|
31
|
+
|
|
32
|
+
export { NkQuote };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { a as NkFormElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-radio name="style" value="concise" checked>Concise</nk-radio>
|
|
4
|
+
// → <label class="nk-check"><input type="radio"> Concise</label>
|
|
5
|
+
//
|
|
6
|
+
// ── Grouping ──
|
|
7
|
+
// Every <nk-radio> keeps its <input type="radio"> in its own shadow root, and
|
|
8
|
+
// native radio grouping works per tree — it does not reach across shadow
|
|
9
|
+
// boundaries. Two <nk-radio name="x"> would therefore both stay checked. So
|
|
10
|
+
// the group is kept here, following the native definition: same `name`,
|
|
11
|
+
// same containing tree, same form owner. One tab stop per group, arrow keys
|
|
12
|
+
// move within it (wrapping, skipping disabled), and select as they go.
|
|
13
|
+
|
|
14
|
+
const ARROW_KEYS = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
|
|
15
|
+
|
|
16
|
+
/** Form owner, also for a peer that has not been upgraded yet. */
|
|
17
|
+
function ownerForm(el) {
|
|
18
|
+
return (el.form !== undefined ? el.form : el.closest('form')) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Only the selected radio is a tab stop; arrow keys move within the group. */
|
|
22
|
+
function syncGroupTabIndex(group) {
|
|
23
|
+
const enabled = group.filter(el => !el.disabled);
|
|
24
|
+
if (!enabled.length) return;
|
|
25
|
+
const focusable = enabled.find(el => el.checked) || enabled[0];
|
|
26
|
+
for (const el of group) {
|
|
27
|
+
if (el._input) el._input.tabIndex = el === focusable ? 0 : -1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class NkRadio extends NkFormElement {
|
|
32
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'required']; }
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
const label = this.createElement('label', ['nk-check']);
|
|
36
|
+
this._input = this.createElement('input', [], { type: 'radio' });
|
|
37
|
+
// `name` on the inner input is inert (it is alone in its tree); it stays
|
|
38
|
+
// as a devtools courtesy. The group above is the real one.
|
|
39
|
+
this._apply();
|
|
40
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
41
|
+
label.appendChild(this._input);
|
|
42
|
+
label.appendChild(document.createElement('slot'));
|
|
43
|
+
this._wrapper.appendChild(label);
|
|
44
|
+
this._syncFormValue();
|
|
45
|
+
// Elements upgrade in document order, so the last `checked` one in the
|
|
46
|
+
// markup wins the group — the same outcome native radios produce.
|
|
47
|
+
if (this._input.checked) this._uncheckPeers();
|
|
48
|
+
syncGroupTabIndex(this._group());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_apply() {
|
|
52
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
53
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
54
|
+
this._input.required = this.getBoolAttr('required');
|
|
55
|
+
const name = this.getAttribute('name');
|
|
56
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
57
|
+
this._input.value = this.getAttribute('value') || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setupEvents() {
|
|
61
|
+
this._onChange = () => this._applyChange();
|
|
62
|
+
this._onKeyDown = (e) => {
|
|
63
|
+
const dir = ARROW_KEYS[e.key];
|
|
64
|
+
if (!dir || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
65
|
+
const group = this._group().filter(el => !el.disabled);
|
|
66
|
+
if (group.length < 2) return;
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const next = group[(group.indexOf(this) + dir + group.length) % group.length];
|
|
69
|
+
next._input.focus();
|
|
70
|
+
next._input.checked = true;
|
|
71
|
+
next._applyChange();
|
|
72
|
+
};
|
|
73
|
+
this._input.addEventListener('change', this._onChange);
|
|
74
|
+
this._input.addEventListener('keydown', this._onKeyDown);
|
|
75
|
+
syncGroupTabIndex(this._group());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
teardownEvents() {
|
|
79
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
80
|
+
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
84
|
+
_applyChange() {
|
|
85
|
+
if (this._input.checked) this._uncheckPeers();
|
|
86
|
+
this._syncing = true;
|
|
87
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
88
|
+
this._syncing = false;
|
|
89
|
+
this._syncFormValue();
|
|
90
|
+
syncGroupTabIndex(this._group());
|
|
91
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
92
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Same name, same tree, same form owner — in document order, this element included. */
|
|
96
|
+
_group() {
|
|
97
|
+
const name = this.getAttribute('name');
|
|
98
|
+
const root = this.getRootNode();
|
|
99
|
+
if (!name || typeof root?.querySelectorAll !== 'function') return [this];
|
|
100
|
+
const form = ownerForm(this);
|
|
101
|
+
return [...root.querySelectorAll('nk-radio')].filter(
|
|
102
|
+
el => el.getAttribute('name') === name && ownerForm(el) === form
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_uncheckPeers() {
|
|
107
|
+
for (const el of this._group()) {
|
|
108
|
+
if (el !== this && el.checked) el.checked = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onAttributeChanged(name) {
|
|
113
|
+
if (this._syncing || !this._input) return;
|
|
114
|
+
this._apply();
|
|
115
|
+
if (name === 'checked' && this._input.checked) this._uncheckPeers();
|
|
116
|
+
this._syncFormValue();
|
|
117
|
+
syncGroupTabIndex(this._group());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_syncFormValue() {
|
|
121
|
+
const val = this.getAttribute('value') || '';
|
|
122
|
+
this.setFormValue(this._input.checked ? val : null, this._input.checked ? 'checked' : 'unchecked');
|
|
123
|
+
this.syncValidityFrom(this._input);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resetValue() {
|
|
127
|
+
this._input.checked = this._defaultChecked;
|
|
128
|
+
this.setBoolAttr('checked', this._defaultChecked);
|
|
129
|
+
this._syncFormValue();
|
|
130
|
+
syncGroupTabIndex(this._group());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
restoreValue(state) {
|
|
134
|
+
if (state === 'checked') this.checked = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); syncGroupTabIndex(this._group()); }
|
|
138
|
+
|
|
139
|
+
focus(options) { this._input?.focus(options); }
|
|
140
|
+
|
|
141
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
142
|
+
set checked(v) {
|
|
143
|
+
if (this._input) this._input.checked = v;
|
|
144
|
+
// Also covers the case where the attribute is already present, so
|
|
145
|
+
// setBoolAttr stays silent and onAttributeChanged never runs.
|
|
146
|
+
if (v) this._uncheckPeers();
|
|
147
|
+
this.setBoolAttr('checked', v);
|
|
148
|
+
if (this._input) { this._syncFormValue(); syncGroupTabIndex(this._group()); }
|
|
149
|
+
}
|
|
150
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
151
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
152
|
+
get name() { return this.getAttribute('name'); }
|
|
153
|
+
set name(v) { this.setAttribute('name', v); }
|
|
154
|
+
get value() { return this.getAttribute('value') || ''; }
|
|
155
|
+
set value(v) { this.setAttribute('value', v); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
customElements.define('nk-radio', NkRadio);
|
|
159
|
+
|
|
160
|
+
export { NkRadio };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.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
|
+
|
|
42
|
+
export { NkSectionLabel };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { a as NkFormElement } from './shared/base-6gEcoatG.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
|
+
|
|
99
|
+
export { NkSegmented };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { a as NkFormElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-select name="role" value="editor">
|
|
4
|
+
// <option value="viewer">Viewer</option>
|
|
5
|
+
// <option value="editor">Editor</option>
|
|
6
|
+
// </nk-select>
|
|
7
|
+
//
|
|
8
|
+
// The light-DOM <option>s are copied into the shadow <select>. A
|
|
9
|
+
// MutationObserver keeps the copy in step when a framework swaps them; the
|
|
10
|
+
// empty string is a valid value; the live selection survives a rebuild.
|
|
11
|
+
class NkSelect extends NkFormElement {
|
|
12
|
+
static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'aria-label']; }
|
|
13
|
+
static get observesLightDom() { return true; }
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
this._select = this.createElement('select', ['nk-select']);
|
|
17
|
+
this._forwardAll();
|
|
18
|
+
this._wrapper.appendChild(this._select);
|
|
19
|
+
this._defaultValue = this.getAttribute('value');
|
|
20
|
+
// Children may not be parsed yet when connectedCallback runs.
|
|
21
|
+
this.projectLightDom();
|
|
22
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_forwardAll() {
|
|
26
|
+
const name = this.getAttribute('name');
|
|
27
|
+
name ? this._select.setAttribute('name', name) : this._select.removeAttribute('name');
|
|
28
|
+
const label = this.getAttribute('aria-label');
|
|
29
|
+
label ? this._select.setAttribute('aria-label', label) : this._select.removeAttribute('aria-label');
|
|
30
|
+
this._select.disabled = this.getBoolAttr('disabled');
|
|
31
|
+
this._select.required = this.getBoolAttr('required');
|
|
32
|
+
this._select.classList.toggle('compact', this.getBoolAttr('compact'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
projectLightDom() {
|
|
36
|
+
if (!this._select) return;
|
|
37
|
+
// The options are pure data — the clones carry no listeners — so skipping
|
|
38
|
+
// an unchanged rebuild is safe, and it keeps an open dropdown from
|
|
39
|
+
// snapping shut on light-DOM churn elsewhere.
|
|
40
|
+
const nodes = [...this.children].filter(n => n.matches('option, optgroup'));
|
|
41
|
+
const signature = nodes.map(n => n.outerHTML).join('');
|
|
42
|
+
if (signature === this._optionSignature) return;
|
|
43
|
+
this._optionSignature = signature;
|
|
44
|
+
|
|
45
|
+
// innerHTML = '' drops the selection; remember it first. selectedIndex -1
|
|
46
|
+
// means "nothing selected", which is not an option whose value is "".
|
|
47
|
+
const previous = this._select.selectedIndex >= 0 ? this._select.value : null;
|
|
48
|
+
|
|
49
|
+
this._select.innerHTML = '';
|
|
50
|
+
for (const node of nodes) this._select.appendChild(node.cloneNode(true));
|
|
51
|
+
|
|
52
|
+
// Keep the live selection when it survived; otherwise the value attribute;
|
|
53
|
+
// otherwise the browser default (first option / `selected`).
|
|
54
|
+
if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
|
|
55
|
+
this._syncFormValue();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Selects `value` if an option carries it, and reports whether it did. */
|
|
59
|
+
_applyValue(value) {
|
|
60
|
+
if (value === null) return false;
|
|
61
|
+
if (![...this._select.options].some(o => o.value === value)) return false;
|
|
62
|
+
this._select.value = value;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setupEvents() {
|
|
67
|
+
this._onChange = () => {
|
|
68
|
+
this._syncFormValue();
|
|
69
|
+
this.emit('nk-change', { value: this._select.value, name: this.name });
|
|
70
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
+
};
|
|
72
|
+
this._select.addEventListener('change', this._onChange);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
teardownEvents() {
|
|
76
|
+
this._select?.removeEventListener('change', this._onChange);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onAttributeChanged(name) {
|
|
80
|
+
if (!this._select) return;
|
|
81
|
+
if (name === 'value') {
|
|
82
|
+
this._applyValue(this.getAttribute('value'));
|
|
83
|
+
this._syncFormValue();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this._forwardAll();
|
|
87
|
+
this._syncFormValue();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_syncFormValue() {
|
|
91
|
+
this.setFormValue(this._select.value);
|
|
92
|
+
this.syncValidityFrom(this._select);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resetValue() {
|
|
96
|
+
if (!this._applyValue(this._defaultValue)) this._select.selectedIndex = 0;
|
|
97
|
+
this._syncFormValue();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
restoreValue(state) {
|
|
101
|
+
if (typeof state === 'string') { this._applyValue(state); this._syncFormValue(); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onFormDisabled(disabled) { this._select.disabled = disabled || this.getBoolAttr('disabled'); }
|
|
105
|
+
|
|
106
|
+
focus(options) { this._select?.focus(options); }
|
|
107
|
+
|
|
108
|
+
get value() { return this._select?.value ?? ''; }
|
|
109
|
+
set value(v) {
|
|
110
|
+
if (this._select) { this._select.value = v; this._syncFormValue(); }
|
|
111
|
+
else this.setAttribute('value', v);
|
|
112
|
+
}
|
|
113
|
+
get selectedIndex() { return this._select?.selectedIndex ?? -1; }
|
|
114
|
+
get options() { return this._select?.options ?? []; }
|
|
115
|
+
get name() { return this.getAttribute('name'); }
|
|
116
|
+
set name(v) { this.setAttribute('name', v); }
|
|
117
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
118
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
119
|
+
get required() { return this.getBoolAttr('required'); }
|
|
120
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
121
|
+
get compact() { return this.getBoolAttr('compact'); }
|
|
122
|
+
set compact(v) { this.setBoolAttr('compact', v); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
customElements.define('nk-select', NkSelect);
|
|
126
|
+
|
|
127
|
+
export { NkSelect };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { N as NkElement } from './shared/base-6gEcoatG.js';
|
|
2
|
+
|
|
3
|
+
// <nk-settings-pane name="profile" label="My profile" icon="👤" group="Account" title="My profile" active>…</nk-settings-pane>
|
|
4
|
+
// → <section class="nk-settings-pane active"><h2>My profile</h2>…</section>
|
|
5
|
+
// `label`, `icon` and `group` feed the nav that <nk-modal> renders; `title`
|
|
6
|
+
// renders the pane heading (child-combinator rule, so it lives here). Slotted
|
|
7
|
+
// <h2>/<h3> are styled by their twins.
|
|
8
|
+
class NkSettingsPane extends NkElement {
|
|
9
|
+
static get observedAttributes() { return ['name', 'label', 'icon', 'group', 'title', 'active']; }
|
|
10
|
+
|
|
11
|
+
render() {
|
|
12
|
+
this._section = this.createElement('section', ['nk-settings-pane'], { role: 'tabpanel' });
|
|
13
|
+
this._h2 = document.createElement('h2');
|
|
14
|
+
this._section.appendChild(this._h2);
|
|
15
|
+
this._section.appendChild(document.createElement('slot'));
|
|
16
|
+
this._wrapper.appendChild(this._section);
|
|
17
|
+
this._sync();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_sync() {
|
|
21
|
+
const title = this.getAttribute('title');
|
|
22
|
+
this._h2.textContent = title || '';
|
|
23
|
+
this._h2.style.display = title ? '' : 'none';
|
|
24
|
+
this._section.classList.toggle('active', this.getBoolAttr('active'));
|
|
25
|
+
this._section.setAttribute('aria-label', this.getAttribute('label') || title || this.getAttribute('name') || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onAttributeChanged(name) {
|
|
29
|
+
this._sync();
|
|
30
|
+
if (['label', 'icon', 'group', 'name'].includes(name)) this.closest('nk-modal')?.requestNav?.();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get name() { return this.getAttribute('name') ?? this.getAttribute('label') ?? ''; }
|
|
34
|
+
get active() { return this.getBoolAttr('active'); }
|
|
35
|
+
set active(v) { this.setBoolAttr('active', v); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
customElements.define('nk-settings-pane', NkSettingsPane);
|
|
39
|
+
|
|
40
|
+
export { NkSettingsPane };
|