@jungherz-de/notionkit-elements 1.5.3 → 1.7.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/README.md +27 -18
- package/dist/components/base.js +346 -0
- package/dist/components/nk-ai-input-row.js +2 -1
- package/dist/components/nk-ai-msg.js +10 -3
- package/dist/components/nk-ai-thread.js +2 -1
- package/dist/components/nk-app.js +2 -1
- package/dist/components/nk-avatar-group.js +2 -1
- package/dist/components/nk-avatar.js +57 -0
- package/dist/components/nk-banner.js +2 -1
- package/dist/components/nk-block-host.js +2 -1
- package/dist/components/nk-board-view.js +4 -2
- package/dist/components/nk-breadcrumb.js +2 -1
- package/dist/components/nk-btn.js +31 -4
- package/dist/components/nk-callout.js +2 -1
- package/dist/components/nk-check.js +2 -1
- package/dist/components/nk-cmdk.js +3 -2
- package/dist/components/nk-code.js +2 -1
- package/dist/components/nk-comment.js +9 -6
- package/dist/components/nk-comments.js +2 -1
- package/dist/components/nk-danger-zone.js +2 -1
- package/dist/components/nk-database.js +16 -6
- package/dist/components/nk-divider.js +2 -1
- package/dist/components/nk-emoji-picker.js +2 -1
- package/dist/components/nk-empty.js +2 -1
- package/dist/components/nk-field.js +2 -1
- package/dist/components/nk-fields.js +2 -1
- package/dist/components/nk-filter-bar.js +59 -37
- package/dist/components/nk-heading.js +2 -1
- package/dist/components/nk-input.js +2 -1
- package/dist/components/nk-kbd.js +2 -1
- package/dist/components/nk-list-view.js +96 -0
- package/dist/components/nk-member-list.js +2 -1
- package/dist/components/nk-member-row.js +10 -6
- package/dist/components/nk-mention.js +2 -1
- package/dist/components/nk-menu-item.js +27 -3
- package/dist/components/nk-menu.js +88 -2
- package/dist/components/nk-modal.js +3 -2
- package/dist/components/nk-model-card.js +2 -1
- package/dist/components/nk-page-actions.js +2 -1
- package/dist/components/nk-page-cover.js +12 -11
- package/dist/components/nk-page-title.js +2 -1
- package/dist/components/nk-page.js +12 -4
- package/dist/components/nk-panel.js +61 -0
- package/dist/components/nk-panels.js +18 -0
- package/dist/components/nk-pop.js +2 -1
- package/dist/components/nk-profile-row.js +2 -1
- package/dist/components/nk-progress.js +5 -2
- package/dist/components/nk-prop.js +54 -0
- package/dist/components/nk-props.js +21 -0
- package/dist/components/nk-quote.js +2 -1
- package/dist/components/nk-radio.js +2 -1
- package/dist/components/nk-section-label.js +2 -1
- package/dist/components/nk-segmented.js +21 -1
- package/dist/components/nk-select.js +2 -1
- package/dist/components/nk-settings-pane.js +2 -1
- package/dist/components/nk-settings-user.js +2 -1
- package/dist/components/nk-sheet.js +97 -0
- package/dist/components/nk-sidebar.js +17 -37
- package/dist/components/nk-skeleton.js +2 -1
- package/dist/components/nk-slider.js +2 -1
- package/dist/components/nk-stat.js +2 -1
- package/dist/components/nk-stats.js +2 -1
- package/dist/components/nk-steps.js +61 -0
- package/dist/components/nk-switch.js +2 -1
- package/dist/components/nk-synced.js +2 -1
- package/dist/components/nk-tab-bar-item.js +2 -1
- package/dist/components/nk-tab-bar.js +2 -1
- package/dist/components/nk-tab.js +2 -1
- package/dist/components/nk-table-view.js +6 -2
- package/dist/components/nk-tabs.js +2 -1
- package/dist/components/nk-tag.js +2 -1
- package/dist/components/nk-template-btn.js +2 -1
- package/dist/components/nk-textarea.js +2 -1
- package/dist/components/nk-theme-toggle.js +2 -1
- package/dist/components/nk-toast.js +2 -1
- package/dist/components/nk-todo.js +2 -1
- package/dist/components/nk-toggle.js +2 -1
- package/dist/components/nk-topbar.js +2 -1
- package/dist/components/nk-tree-item.js +2 -1
- package/dist/components/nk-tree.js +2 -1
- package/dist/components/nk-workspace-switcher.js +2 -1
- package/dist/components/shared/avatar-BiqCaHOt.js +19 -0
- package/dist/components/shared/{focus-DXrK9oLe.js → focus-BNAChOXO.js} +12 -4
- package/dist/components/shared/{property-cell-CtXQpUjQ.js → property-cell-o7N30kAQ.js} +8 -3
- package/dist/notionkit-elements.esm.js +759 -120
- package/dist/notionkit-elements.js +767 -119
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +7 -6
- package/src/components/content/nk-progress.js +3 -1
- package/src/components/data/nk-ai-msg.js +8 -2
- package/src/components/data/nk-comment.js +7 -5
- package/src/components/data/nk-database.js +14 -5
- package/src/components/data/nk-filter-bar.js +57 -36
- package/src/components/data/nk-list-view.js +93 -0
- package/src/components/data/nk-table-view.js +2 -0
- package/src/components/forms/nk-btn.js +29 -3
- package/src/components/overlays/nk-menu-item.js +25 -2
- package/src/components/overlays/nk-menu.js +86 -1
- package/src/components/overlays/nk-sheet.js +95 -0
- package/src/components/page/nk-avatar.js +55 -0
- package/src/components/page/nk-member-row.js +8 -5
- package/src/components/page/nk-page-cover.js +10 -10
- package/src/components/page/nk-page.js +10 -3
- package/src/components/page/nk-panel.js +59 -0
- package/src/components/page/nk-panels.js +16 -0
- package/src/components/page/nk-prop.js +52 -0
- package/src/components/page/nk-props.js +19 -0
- package/src/components/page/nk-segmented.js +19 -0
- package/src/components/page/nk-steps.js +59 -0
- package/src/components/shell/nk-sidebar.js +15 -36
- package/src/index.js +18 -0
- package/src/util/avatar.js +17 -0
- package/src/util/focus.js +11 -3
- package/src/util/property-cell.js +7 -3
- package/dist/components/shared/base-C3eJwHKA.js +0 -366
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { deepActiveElement } from '../../util/focus.js';
|
|
2
3
|
|
|
3
4
|
// <nk-menu>
|
|
4
5
|
// <nk-menu-item type="label">Page</nk-menu-item>
|
|
@@ -8,13 +9,79 @@ import { NkElement } from '../../base.js';
|
|
|
8
9
|
// </nk-menu>
|
|
9
10
|
// → <div class="nk-pop nk-menu" role="menu">…</div>
|
|
10
11
|
// ↑↓ move between items, Enter selects, nk-select bubbles from the item.
|
|
12
|
+
//
|
|
13
|
+
// <nk-menu floating sheet> is a menu over the page (NotionKit 1.7.0): it
|
|
14
|
+
// fades in like the palette when `open` is set and is a bottom sheet on a
|
|
15
|
+
// phone. The host becomes the fixed box you position – show(anchor) puts it
|
|
16
|
+
// under the anchor, right edges aligned (align="start": left edges). A tap
|
|
17
|
+
// outside closes it and reaches nothing else; Escape and a chosen item
|
|
18
|
+
// close it too, a switch row does not. Opened from the keyboard, focus moves
|
|
19
|
+
// to the first item; closing returns it to where it was.
|
|
20
|
+
const floatSheet = new CSSStyleSheet();
|
|
21
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
|
|
22
|
+
|
|
23
|
+
/** The border box of an element, or of the first descendant with one (display: contents hosts). */
|
|
24
|
+
function boxOf(el) {
|
|
25
|
+
if (el.getClientRects().length) return el.getBoundingClientRect();
|
|
26
|
+
for (const child of [...(el.shadowRoot?.children ?? []), ...el.children]) {
|
|
27
|
+
const r = boxOf(child);
|
|
28
|
+
if (r.width || r.height) return r;
|
|
29
|
+
}
|
|
30
|
+
return el.getBoundingClientRect();
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
class NkMenu extends NkElement {
|
|
34
|
+
static get hostStyles() { return floatSheet; }
|
|
35
|
+
static get observedAttributes() { return ['floating', 'sheet', 'open', 'align']; }
|
|
36
|
+
|
|
12
37
|
render() {
|
|
13
38
|
this._box = this.createElement('div', ['nk-pop', 'nk-menu'], { role: 'menu' });
|
|
14
39
|
this._box.appendChild(document.createElement('slot'));
|
|
15
40
|
this._wrapper.appendChild(this._box);
|
|
41
|
+
this._syncMode();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_syncMode() {
|
|
45
|
+
this._box.classList.toggle('floating', this.getBoolAttr('floating'));
|
|
46
|
+
this._box.classList.toggle('sheet', this.getBoolAttr('sheet'));
|
|
47
|
+
this._box.classList.toggle('open', this.getBoolAttr('open'));
|
|
16
48
|
}
|
|
17
49
|
|
|
50
|
+
onAttributeChanged(name) {
|
|
51
|
+
this._syncMode();
|
|
52
|
+
if (name !== 'open') return;
|
|
53
|
+
const open = this.getBoolAttr('open');
|
|
54
|
+
// aria-expanded belongs on the control itself, inside a host such as <nk-btn>.
|
|
55
|
+
if (this._anchor) (this._anchor.shadowRoot?.querySelector('button, a[href]') ?? this._anchor).setAttribute('aria-expanded', String(open));
|
|
56
|
+
if (open) {
|
|
57
|
+
this._returnFocus = deepActiveElement();
|
|
58
|
+
if (this._returnFocus?.matches(':focus-visible')) requestAnimationFrame(() => this.focusFirst());
|
|
59
|
+
document.addEventListener('click', this._onOutside, true);
|
|
60
|
+
} else {
|
|
61
|
+
document.removeEventListener('click', this._onOutside, true);
|
|
62
|
+
const back = this._returnFocus;
|
|
63
|
+
this._returnFocus = null;
|
|
64
|
+
if (back?.isConnected && this.contains(document.activeElement)) back.focus({ preventScroll: true });
|
|
65
|
+
}
|
|
66
|
+
this.emit('nk-toggle', { open });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Opens under `anchor` (any element, display: contents hosts included). */
|
|
70
|
+
show(anchor) {
|
|
71
|
+
if (anchor) {
|
|
72
|
+
this._anchor = anchor;
|
|
73
|
+
const r = boxOf(anchor);
|
|
74
|
+
this.style.top = `${r.bottom + 6}px`;
|
|
75
|
+
if (this.getAttribute('align') === 'start') { this.style.left = `${r.left}px`; this.style.right = ''; }
|
|
76
|
+
else { this.style.right = `${Math.max(8, innerWidth - r.right)}px`; this.style.left = ''; }
|
|
77
|
+
}
|
|
78
|
+
this.setBoolAttr('open', true);
|
|
79
|
+
}
|
|
80
|
+
close() { this.setBoolAttr('open', false); }
|
|
81
|
+
toggle(anchor) { this.getBoolAttr('open') ? this.close() : this.show(anchor); }
|
|
82
|
+
get open() { return this.getBoolAttr('open'); }
|
|
83
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
84
|
+
|
|
18
85
|
get items() { return [...this.querySelectorAll(':scope > nk-menu-item')].filter(i => i.selectable); }
|
|
19
86
|
|
|
20
87
|
setupEvents() {
|
|
@@ -32,9 +99,27 @@ class NkMenu extends NkElement {
|
|
|
32
99
|
}
|
|
33
100
|
};
|
|
34
101
|
this.addEventListener('keydown', this._onKey);
|
|
102
|
+
// Floating: outside taps close and go nowhere else; Escape and a chosen item close.
|
|
103
|
+
this._onOutside = (e) => {
|
|
104
|
+
const path = e.composedPath();
|
|
105
|
+
if (path.includes(this) || (this._anchor && path.includes(this._anchor))) return;
|
|
106
|
+
e.preventDefault(); e.stopPropagation();
|
|
107
|
+
this.close();
|
|
108
|
+
};
|
|
109
|
+
// Captured, so a menu inside a modal or sheet closes before they do.
|
|
110
|
+
this._onEscape = (e) => { if (e.key === 'Escape' && this.getBoolAttr('floating') && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
111
|
+
this._onSelect = () => { if (this.getBoolAttr('floating')) this.close(); };
|
|
112
|
+
document.addEventListener('keydown', this._onEscape, true);
|
|
113
|
+
this.addEventListener('nk-select', this._onSelect);
|
|
114
|
+
if (this.getBoolAttr('open')) document.addEventListener('click', this._onOutside, true);
|
|
35
115
|
}
|
|
36
116
|
|
|
37
|
-
teardownEvents() {
|
|
117
|
+
teardownEvents() {
|
|
118
|
+
this.removeEventListener('keydown', this._onKey);
|
|
119
|
+
document.removeEventListener('keydown', this._onEscape, true);
|
|
120
|
+
this.removeEventListener('nk-select', this._onSelect);
|
|
121
|
+
document.removeEventListener('click', this._onOutside, true);
|
|
122
|
+
}
|
|
38
123
|
|
|
39
124
|
/** Focuses the first item (called by a popover when it opens). */
|
|
40
125
|
focusFirst() { this.items[0]?.focus(); }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
import { lockScroll, unlockScroll, inertOutside, firstFocusable, deepActiveElement } from '../../util/focus.js';
|
|
3
|
+
|
|
4
|
+
// <nk-sheet id="more" title="More">
|
|
5
|
+
// <nk-tree manual>
|
|
6
|
+
// <nk-section-label>Favourites</nk-section-label>
|
|
7
|
+
// <nk-tree-item icon="🚀">NotionKit MVP</nk-tree-item>
|
|
8
|
+
// </nk-tree>
|
|
9
|
+
// </nk-sheet>
|
|
10
|
+
// → <div class="nk-sheet-backdrop open"><div class="nk-sheet" role="dialog">
|
|
11
|
+
// <div class="sh-grabber"></div><div class="sh-title">More</div>…</div></div>
|
|
12
|
+
//
|
|
13
|
+
// A bottom sheet (NotionKit 1.7.0) with nk-modal's contract: show(), close()
|
|
14
|
+
// and toggle(), Escape and the backdrop close it, focus moves in and back
|
|
15
|
+
// out, the page behind is scroll-locked and inert. It lies above the tab
|
|
16
|
+
// bar; rows inside are 40px high. `title` is the heading shown under the
|
|
17
|
+
// grabber and the dialog's name, never a tooltip – see NkElement.takeTitle().
|
|
18
|
+
// Choosing a row does not close the sheet – the app decides (nk-select
|
|
19
|
+
// bubbles out). Put the element directly under <body>: a transformed
|
|
20
|
+
// ancestor would trap the fixed backdrop.
|
|
21
|
+
class NkSheet extends NkElement {
|
|
22
|
+
static get observedAttributes() { return ['open', 'title']; }
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
this.takeTitle();
|
|
26
|
+
this._backdrop = this.createElement('div', ['nk-sheet-backdrop']);
|
|
27
|
+
this._backdrop.inert = true;
|
|
28
|
+
this._box = this.createElement('div', ['nk-sheet'], { role: 'dialog', 'aria-modal': 'true', tabindex: '-1' });
|
|
29
|
+
this._heading = this.createElement('div', ['sh-title'], { id: 'title' });
|
|
30
|
+
this._box.append(this.createElement('div', ['sh-grabber']), this._heading, document.createElement('slot'));
|
|
31
|
+
this._backdrop.appendChild(this._box);
|
|
32
|
+
this._wrapper.appendChild(this._backdrop);
|
|
33
|
+
this._syncTitle();
|
|
34
|
+
this._syncOpen();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_syncTitle() {
|
|
38
|
+
const text = this._titleText || '';
|
|
39
|
+
this._heading.textContent = text;
|
|
40
|
+
this._heading.hidden = !text;
|
|
41
|
+
if (text) this._box.setAttribute('aria-labelledby', 'title');
|
|
42
|
+
else this._box.removeAttribute('aria-labelledby');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_syncOpen() {
|
|
46
|
+
const open = this.getBoolAttr('open');
|
|
47
|
+
this._backdrop.classList.toggle('open', open);
|
|
48
|
+
this._backdrop.inert = !open;
|
|
49
|
+
if (open === this._wasOpen) return;
|
|
50
|
+
this._wasOpen = open;
|
|
51
|
+
if (open) {
|
|
52
|
+
this._returnFocus = deepActiveElement();
|
|
53
|
+
lockScroll();
|
|
54
|
+
this._undoInert = inertOutside(this);
|
|
55
|
+
requestAnimationFrame(() => (firstFocusable(this) || this._box).focus({ preventScroll: true }));
|
|
56
|
+
} else {
|
|
57
|
+
unlockScroll();
|
|
58
|
+
this._undoInert?.(); this._undoInert = null;
|
|
59
|
+
const back = this._returnFocus;
|
|
60
|
+
if (back && typeof back.focus === 'function' && back.isConnected) back.focus({ preventScroll: true });
|
|
61
|
+
this._returnFocus = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setupEvents() {
|
|
66
|
+
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
67
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
68
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
69
|
+
document.addEventListener('keydown', this._onKey);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
teardownEvents() {
|
|
73
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
74
|
+
document.removeEventListener('keydown', this._onKey);
|
|
75
|
+
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onAttributeChanged(name, oldValue, value) {
|
|
79
|
+
if (name === 'title') { if (this.takeTitle(value)) this._syncTitle(); return; }
|
|
80
|
+
this._syncOpen();
|
|
81
|
+
this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
show() { this.setBoolAttr('open', true); }
|
|
85
|
+
close() { this.setBoolAttr('open', false); }
|
|
86
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
87
|
+
|
|
88
|
+
get open() { return this.getBoolAttr('open'); }
|
|
89
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
90
|
+
get title() { return this._titleText ?? ''; }
|
|
91
|
+
set title(v) { this._titleText = v == null ? '' : String(v); if (this._initialized) this._syncTitle(); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
customElements.define('nk-sheet', NkSheet);
|
|
95
|
+
export { NkSheet };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
import { paintAvatar, initialsOf } from '../../util/avatar.js';
|
|
3
|
+
|
|
4
|
+
// <nk-avatar>MK</nk-avatar> → <span class="nk-avatar">MK</span>
|
|
5
|
+
// <nk-avatar size="small" color="green">SL</nk-avatar>
|
|
6
|
+
// <nk-avatar name="Ada Lovelace" size="large"></nk-avatar> → initials "AL"
|
|
7
|
+
// <nk-avatar src="ada.jpg" name="Ada Lovelace" size="xlarge"></nk-avatar>
|
|
8
|
+
// <nk-avatar square>A</nk-avatar> → a workspace icon
|
|
9
|
+
// size: small (20px) · default 24px · large (32px) · xlarge (56px). color: one
|
|
10
|
+
// of Notion's nine names, or any CSS background. Without content the initials
|
|
11
|
+
// come from `name`; with `src` a photo fills the circle and `name` is its alt.
|
|
12
|
+
class NkAvatar extends NkElement {
|
|
13
|
+
static get observedAttributes() { return ['size', 'color', 'square', 'src', 'name']; }
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
this._box = this.createElement('span', ['nk-avatar']);
|
|
17
|
+
this._slot = document.createElement('slot');
|
|
18
|
+
this._initials = document.createTextNode('');
|
|
19
|
+
this._slot.appendChild(this._initials);
|
|
20
|
+
this._img = this.createElement('img', [], { alt: '' });
|
|
21
|
+
this._box.appendChild(this._slot);
|
|
22
|
+
this._wrapper.appendChild(this._box);
|
|
23
|
+
this._sync();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_sync() {
|
|
27
|
+
const size = this.getAttribute('size');
|
|
28
|
+
for (const s of ['small', 'large', 'xlarge']) this._box.classList.toggle(s, s === size);
|
|
29
|
+
this._box.classList.toggle('square', this.getBoolAttr('square'));
|
|
30
|
+
paintAvatar(this._box, this.getAttribute('color'));
|
|
31
|
+
const name = this.getAttribute('name');
|
|
32
|
+
this._initials.data = initialsOf(name);
|
|
33
|
+
const src = this.getAttribute('src');
|
|
34
|
+
if (src) {
|
|
35
|
+
this._img.src = src;
|
|
36
|
+
this._img.alt = name || '';
|
|
37
|
+
if (this._slot.isConnected) this._slot.replaceWith(this._img);
|
|
38
|
+
} else if (this._img.isConnected) this._img.replaceWith(this._slot);
|
|
39
|
+
// A picture or initials with a name speak for themselves; a bare glyph is decoration.
|
|
40
|
+
if (name && !src) { this._box.setAttribute('role', 'img'); this._box.setAttribute('aria-label', name); }
|
|
41
|
+
else { this._box.removeAttribute('role'); this._box.removeAttribute('aria-label'); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onAttributeChanged() { this._sync(); }
|
|
45
|
+
|
|
46
|
+
get size() { return this.getAttribute('size'); }
|
|
47
|
+
set size(v) { v == null ? this.removeAttribute('size') : this.setAttribute('size', v); }
|
|
48
|
+
get color() { return this.getAttribute('color'); }
|
|
49
|
+
set color(v) { v == null ? this.removeAttribute('color') : this.setAttribute('color', v); }
|
|
50
|
+
get src() { return this.getAttribute('src'); }
|
|
51
|
+
set src(v) { v == null ? this.removeAttribute('src') : this.setAttribute('src', v); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
customElements.define('nk-avatar', NkAvatar);
|
|
55
|
+
export { NkAvatar };
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { paintAvatar, initialsOf } from '../../util/avatar.js';
|
|
2
3
|
|
|
3
|
-
// <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="
|
|
4
|
+
// <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="green">
|
|
4
5
|
// <nk-select slot="role" compact>…</nk-select>
|
|
5
6
|
// </nk-member-row>
|
|
6
|
-
// → <div class="nk-member-row"><span class="
|
|
7
|
+
// → <div class="nk-member-row"><span class="nk-avatar green">SL</span><div>Sara Lindt<div class="m-mail">…</div></div><select …></div>
|
|
8
|
+
// color: one of Notion's nine names, or any CSS background; without it the
|
|
9
|
+
// avatar gradient. The row sizes the avatar to 28px.
|
|
7
10
|
class NkMemberRow extends NkElement {
|
|
8
11
|
static get observedAttributes() { return ['name', 'mail', 'avatar', 'color', 'last']; }
|
|
9
12
|
|
|
10
13
|
render() {
|
|
11
14
|
this._row = this.createElement('div', ['nk-member-row']);
|
|
12
|
-
this._avatar = this.createElement('span', ['
|
|
15
|
+
this._avatar = this.createElement('span', ['nk-avatar']);
|
|
13
16
|
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
14
17
|
avatarSlot.appendChild(this._avatar);
|
|
15
18
|
const text = document.createElement('div');
|
|
@@ -33,8 +36,8 @@ class NkMemberRow extends NkElement {
|
|
|
33
36
|
this._mail.textContent = mail || '';
|
|
34
37
|
this._mail.style.display = mail ? '' : 'none';
|
|
35
38
|
const avatar = this.getAttribute('avatar');
|
|
36
|
-
this._avatar.textContent = avatar || (this.getAttribute('name')
|
|
37
|
-
this._avatar
|
|
39
|
+
this._avatar.textContent = avatar || initialsOf(this.getAttribute('name'));
|
|
40
|
+
paintAvatar(this._avatar, this.getAttribute('color'));
|
|
38
41
|
this._row.classList.toggle('last', this.getBoolAttr('last'));
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
2
|
|
|
3
3
|
// <nk-page-cover slot="cover"></nk-page-cover> → the token gradient
|
|
4
|
-
// <nk-page-cover slot="cover" src="cover.jpg"></nk-page-cover> →
|
|
4
|
+
// <nk-page-cover slot="cover" src="cover.jpg" position="center 30%"></nk-page-cover> → a picture
|
|
5
|
+
// → <div class="nk-cover"><img src="cover.jpg" alt=""></div>: the picture fills
|
|
6
|
+
// the band, cropped rather than stretched; `position` moves the crop, as
|
|
7
|
+
// Notion's "Reposition" does. The gradient stays underneath while it loads.
|
|
5
8
|
class NkPageCover extends NkElement {
|
|
6
|
-
static get observedAttributes() { return ['src']; }
|
|
9
|
+
static get observedAttributes() { return ['src', 'position']; }
|
|
7
10
|
|
|
8
11
|
render() {
|
|
9
12
|
this._cover = this.createElement('div', ['nk-cover']);
|
|
13
|
+
this._img = this.createElement('img', [], { alt: '' });
|
|
10
14
|
this._wrapper.appendChild(this._cover);
|
|
11
15
|
this._sync();
|
|
12
16
|
}
|
|
@@ -14,14 +18,10 @@ class NkPageCover extends NkElement {
|
|
|
14
18
|
_sync() {
|
|
15
19
|
const src = this.getAttribute('src');
|
|
16
20
|
if (src) {
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
this._cover.
|
|
20
|
-
} else
|
|
21
|
-
this._cover.style.backgroundImage = '';
|
|
22
|
-
this._cover.style.backgroundSize = '';
|
|
23
|
-
this._cover.style.backgroundPosition = '';
|
|
24
|
-
}
|
|
21
|
+
this._img.src = src;
|
|
22
|
+
this._img.style.objectPosition = this.getAttribute('position') || '';
|
|
23
|
+
if (!this._img.isConnected) this._cover.appendChild(this._img);
|
|
24
|
+
} else this._img.remove();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
onAttributeChanged() { this._sync(); }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
-
// <nk-page icon="🚀" cover>
|
|
3
|
+
// <nk-page icon="🚀" cover full small>
|
|
4
4
|
// <nk-page-title>NotionKit MVP</nk-page-title>
|
|
5
5
|
// <nk-page-actions>…</nk-page-actions>
|
|
6
6
|
// <p class="lead">…</p>
|
|
@@ -12,9 +12,10 @@ import { NkElement } from '../../base.js';
|
|
|
12
12
|
// The host is display:contents, so .nk-page-scroll is the flex child of
|
|
13
13
|
// .nk-main that scrolls. `narrow` drops the scroll wrapper for pages that are
|
|
14
14
|
// the document itself (landing / docs skeleton). The page icon is rendered
|
|
15
|
-
// here because its ::slotted twin is keyed on the parent (.nk-page).
|
|
15
|
+
// here because its ::slotted twin is keyed on the parent (.nk-page). `full` and
|
|
16
|
+
// `small` are Notion's page options – full width, 14px text – set on the page.
|
|
16
17
|
class NkPage extends NkElement {
|
|
17
|
-
static get observedAttributes() { return ['icon', 'cover', 'narrow']; }
|
|
18
|
+
static get observedAttributes() { return ['icon', 'cover', 'narrow', 'full', 'small']; }
|
|
18
19
|
|
|
19
20
|
render() {
|
|
20
21
|
this._page = this.createElement('div', ['nk-page']);
|
|
@@ -51,6 +52,8 @@ class NkPage extends NkElement {
|
|
|
51
52
|
// (attribute or a slotted nk-page-cover) the icon sits in the padding.
|
|
52
53
|
const slotted = this._coverSlot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.data.trim());
|
|
53
54
|
this._page.classList.toggle('covered', this.getBoolAttr('cover') || slotted);
|
|
55
|
+
this._page.classList.toggle('full', this.getBoolAttr('full'));
|
|
56
|
+
this._page.classList.toggle('small', this.getBoolAttr('small'));
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
setupEvents() {
|
|
@@ -72,6 +75,10 @@ class NkPage extends NkElement {
|
|
|
72
75
|
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
73
76
|
get cover() { return this.getBoolAttr('cover'); }
|
|
74
77
|
set cover(v) { this.setBoolAttr('cover', v); }
|
|
78
|
+
get full() { return this.getBoolAttr('full'); }
|
|
79
|
+
set full(v) { this.setBoolAttr('full', v); }
|
|
80
|
+
get small() { return this.getBoolAttr('small'); }
|
|
81
|
+
set small(v) { this.setBoolAttr('small', v); }
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
customElements.define('nk-page', NkPage);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-panel title="Weekly review"><p>Three pages changed.</p></nk-panel>
|
|
4
|
+
// <nk-panel href="/roadmap" cover="covers/aurora.svg" icon="🚀" title="Roadmap"><p>2 min ago</p></nk-panel>
|
|
5
|
+
// → <a class="nk-panel" href="/roadmap"><div class="nk-cover"><img src="…" alt=""></div>
|
|
6
|
+
// <div class="nk-page-icon">🚀</div><h3>Roadmap</h3>…</a>
|
|
7
|
+
// `cover` without a value draws the token gradient, with one a picture; `icon`
|
|
8
|
+
// overlaps the cover as on a page – a page tile, as on Notion's Home. With
|
|
9
|
+
// `href` the panel is a link. `title` is the heading, never a tooltip.
|
|
10
|
+
class NkPanel extends NkElement {
|
|
11
|
+
static get observedAttributes() { return ['title', 'icon', 'cover', 'href', 'target']; }
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
this._cover = this.createElement('div', ['nk-cover']);
|
|
15
|
+
this._img = this.createElement('img', [], { alt: '' });
|
|
16
|
+
this._icon = this.createElement('div', ['nk-page-icon']);
|
|
17
|
+
this._heading = document.createElement('h3');
|
|
18
|
+
this._slot = document.createElement('slot');
|
|
19
|
+
this.takeTitle();
|
|
20
|
+
this._build();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_build() {
|
|
24
|
+
const href = this.getAttribute('href');
|
|
25
|
+
const box = this.createElement(href ? 'a' : 'div', ['nk-panel']);
|
|
26
|
+
box.append(this._cover, this._icon, this._heading, this._slot);
|
|
27
|
+
this._wrapper.replaceChildren(box);
|
|
28
|
+
this._box = box;
|
|
29
|
+
this._sync();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_sync() {
|
|
33
|
+
const href = this.getAttribute('href');
|
|
34
|
+
if (href) { this._box.setAttribute('href', href); this._box.target = this.getAttribute('target') || ''; }
|
|
35
|
+
const cover = this.getAttribute('cover');
|
|
36
|
+
this._cover.style.display = cover === null ? 'none' : '';
|
|
37
|
+
if (cover) { this._img.src = cover; if (!this._img.isConnected) this._cover.appendChild(this._img); }
|
|
38
|
+
else this._img.remove();
|
|
39
|
+
const icon = this.getAttribute('icon');
|
|
40
|
+
this._icon.textContent = icon || '';
|
|
41
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
42
|
+
this._heading.textContent = this._titleText || '';
|
|
43
|
+
this._heading.style.display = this._titleText ? '' : 'none';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onAttributeChanged(name, _old, value) {
|
|
47
|
+
if (name === 'title' && !this.takeTitle(value)) return;
|
|
48
|
+
if (name === 'href' && (value === null) !== (this._box.localName === 'div')) { this._build(); return; }
|
|
49
|
+
this._sync();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get title() { return this._titleText ?? ''; }
|
|
53
|
+
set title(v) { this._titleText = v == null ? '' : String(v); if (this._initialized) this._sync(); }
|
|
54
|
+
get href() { return this.getAttribute('href'); }
|
|
55
|
+
set href(v) { v == null ? this.removeAttribute('href') : this.setAttribute('href', v); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
customElements.define('nk-panel', NkPanel);
|
|
59
|
+
export { NkPanel };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-panels><nk-panel …></nk-panel><nk-panel …></nk-panel></nk-panels>
|
|
4
|
+
// → <div class="nk-panels">…</div>: columns of at least 200px that share the
|
|
5
|
+
// row and fall to one on a phone. The <nk-panel> hosts are display: contents,
|
|
6
|
+
// so each .nk-panel is a cell of this grid.
|
|
7
|
+
class NkPanels extends NkElement {
|
|
8
|
+
render() {
|
|
9
|
+
const grid = this.createElement('div', ['nk-panels']);
|
|
10
|
+
grid.appendChild(document.createElement('slot'));
|
|
11
|
+
this._wrapper.appendChild(grid);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
customElements.define('nk-panels', NkPanels);
|
|
16
|
+
export { NkPanels };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-prop label="Due" icon="📅">2 June 2026</nk-prop>
|
|
4
|
+
// → <div class="nk-prop"><dt class="p-name"><span class="p-icon">📅</span>Due</dt><dd class="p-value">2 June 2026</dd></div>
|
|
5
|
+
// The value is the element's content – tags, an avatar and a name, a date, a
|
|
6
|
+
// <nk-progress wide>. slot="icon" replaces the icon glyph. A click on the
|
|
7
|
+
// name or the value fires nk-action { action: 'name' | 'value', label }, the
|
|
8
|
+
// moment Notion opens the property's editor.
|
|
9
|
+
class NkProp extends NkElement {
|
|
10
|
+
static get observedAttributes() { return ['label', 'icon']; }
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
this._row = this.createElement('div', ['nk-prop']);
|
|
14
|
+
this._name = this.createElement('dt', ['p-name']);
|
|
15
|
+
this._icon = this.createElement('span', ['p-icon']);
|
|
16
|
+
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
17
|
+
iconSlot.appendChild(this._icon);
|
|
18
|
+
this._label = document.createTextNode('');
|
|
19
|
+
this._name.append(iconSlot, this._label);
|
|
20
|
+
this._value = this.createElement('dd', ['p-value']);
|
|
21
|
+
this._value.appendChild(document.createElement('slot'));
|
|
22
|
+
this._row.append(this._name, this._value);
|
|
23
|
+
this._wrapper.appendChild(this._row);
|
|
24
|
+
this._sync();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_sync() {
|
|
28
|
+
const icon = this.getAttribute('icon');
|
|
29
|
+
this._icon.textContent = icon || '';
|
|
30
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
31
|
+
this._label.data = this.getAttribute('label') || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setupEvents() {
|
|
35
|
+
this._onClick = (e) => {
|
|
36
|
+
const part = e.composedPath().includes(this._name) ? 'name' : 'value';
|
|
37
|
+
this.emit('nk-action', { action: part, label: this.label });
|
|
38
|
+
};
|
|
39
|
+
this._row.addEventListener('click', this._onClick);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
teardownEvents() { this._row?.removeEventListener('click', this._onClick); }
|
|
43
|
+
onAttributeChanged() { this._sync(); }
|
|
44
|
+
|
|
45
|
+
get label() { return this.getAttribute('label') || ''; }
|
|
46
|
+
set label(v) { this.setAttribute('label', v); }
|
|
47
|
+
get icon() { return this.getAttribute('icon'); }
|
|
48
|
+
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
customElements.define('nk-prop', NkProp);
|
|
52
|
+
export { NkProp };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-props>
|
|
4
|
+
// <nk-prop label="Status" icon="◉"><nk-tag color="blue">In progress</nk-tag></nk-prop>
|
|
5
|
+
// <nk-prop label="Owner" icon="👤"><nk-avatar size="small">MK</nk-avatar>Marcel Karas</nk-prop>
|
|
6
|
+
// </nk-props>
|
|
7
|
+
// → <dl class="nk-props">…</dl> – the properties under a page title. The rows
|
|
8
|
+
// are <nk-prop> hosts with display: contents, so each .nk-prop is a row of
|
|
9
|
+
// this list.
|
|
10
|
+
class NkProps extends NkElement {
|
|
11
|
+
render() {
|
|
12
|
+
const list = this.createElement('dl', ['nk-props']);
|
|
13
|
+
list.appendChild(document.createElement('slot'));
|
|
14
|
+
this._wrapper.appendChild(list);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
customElements.define('nk-props', NkProps);
|
|
19
|
+
export { NkProps };
|
|
@@ -6,6 +6,9 @@ import { NkFormElement } from '../../base.js';
|
|
|
6
6
|
// → <div class="nk-segmented">…</div> with .active on the chosen button.
|
|
7
7
|
// The buttons stay in the light DOM (their look comes from the ::slotted
|
|
8
8
|
// twins); the element sets type/role, moves .active and submits the value.
|
|
9
|
+
// With `scroll` the chosen option is kept in view – after the first layout,
|
|
10
|
+
// on every change and when the row's width changes – by scrolling the row
|
|
11
|
+
// alone, never the page, by a relative distance, so right-to-left works too.
|
|
9
12
|
const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
10
13
|
|
|
11
14
|
class NkSegmented extends NkFormElement {
|
|
@@ -49,6 +52,19 @@ class NkSegmented extends NkFormElement {
|
|
|
49
52
|
}
|
|
50
53
|
this._value = value;
|
|
51
54
|
this.setFormValue(value);
|
|
55
|
+
this._reveal(this._revealed ? 'smooth' : 'instant');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Scrolls the row the least distance that shows the chosen button whole. */
|
|
59
|
+
_reveal(behavior) {
|
|
60
|
+
if (!this.getBoolAttr('scroll') || !this._box?.clientWidth) return;
|
|
61
|
+
const btn = this.buttons.find(b => b.classList.contains('active'));
|
|
62
|
+
if (!btn) return;
|
|
63
|
+
const row = this._box.getBoundingClientRect(), r = btn.getBoundingClientRect(), pad = 8;
|
|
64
|
+
const delta = r.left < row.left ? r.left - row.left - pad : r.right > row.right ? r.right - row.right + pad : 0;
|
|
65
|
+
const smooth = behavior === 'smooth' && !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
66
|
+
if (delta) this._box.scrollBy({ left: delta, behavior: smooth ? 'smooth' : 'instant' });
|
|
67
|
+
this._revealed = true;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
setupEvents() {
|
|
@@ -72,6 +88,8 @@ class NkSegmented extends NkFormElement {
|
|
|
72
88
|
this._slot.addEventListener('slotchange', this._onSlot);
|
|
73
89
|
this.addEventListener('click', this._onClick);
|
|
74
90
|
this.addEventListener('keydown', this._onKey);
|
|
91
|
+
this._resize = new ResizeObserver(() => this._reveal('instant'));
|
|
92
|
+
this._resize.observe(this._box);
|
|
75
93
|
this._sync();
|
|
76
94
|
}
|
|
77
95
|
|
|
@@ -79,6 +97,7 @@ class NkSegmented extends NkFormElement {
|
|
|
79
97
|
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
80
98
|
this.removeEventListener('click', this._onClick);
|
|
81
99
|
this.removeEventListener('keydown', this._onKey);
|
|
100
|
+
this._resize?.disconnect();
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
_choose(value) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-steps label="Connect your own model" current="2"
|
|
4
|
+
// steps="Choose a provider, Enter the API key, Test the connection"></nk-steps>
|
|
5
|
+
// steps.steps = [{ label: 'Choose a provider', desc: 'Anthropic' }, 'Enter the API key', …];
|
|
6
|
+
// → <ol class="nk-steps" aria-label="…">
|
|
7
|
+
// <li class="nk-step done"><span class="st-mark">✓</span><span>Choose a provider<span class="st-desc">Anthropic</span></span></li>
|
|
8
|
+
// <li class="nk-step current" aria-current="step"><span class="st-mark">2</span><span>Enter the API key</span></li>
|
|
9
|
+
// <li class="nk-step"><span class="st-mark">3</span><span>Test the connection</span></li></ol>
|
|
10
|
+
// A setup in a few steps (NotionKit 1.7.0). `current` counts from 1; the
|
|
11
|
+
// steps before it are done, one past the last marks all of them done. The
|
|
12
|
+
// `steps` attribute is a comma-separated list; the property also takes
|
|
13
|
+
// { label, desc } objects for a line under the label.
|
|
14
|
+
class NkSteps extends NkElement {
|
|
15
|
+
static get observedAttributes() { return ['steps', 'current', 'label']; }
|
|
16
|
+
|
|
17
|
+
render() {
|
|
18
|
+
this._list = this.createElement('ol', ['nk-steps']);
|
|
19
|
+
this._wrapper.appendChild(this._list);
|
|
20
|
+
this._sync();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_items() {
|
|
24
|
+
if (this._steps) return this._steps;
|
|
25
|
+
return (this.getAttribute('steps') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_sync() {
|
|
29
|
+
const label = this.getAttribute('label');
|
|
30
|
+
if (label) this._list.setAttribute('aria-label', label); else this._list.removeAttribute('aria-label');
|
|
31
|
+
const current = this.current;
|
|
32
|
+
this._list.replaceChildren(...this._items().map((step, i) => {
|
|
33
|
+
const n = i + 1, { label: text, desc } = typeof step === 'object' && step ? step : { label: String(step) };
|
|
34
|
+
const li = this.createElement('li', ['nk-step']);
|
|
35
|
+
li.classList.toggle('done', n < current);
|
|
36
|
+
li.classList.toggle('current', n === current);
|
|
37
|
+
if (n === current) li.setAttribute('aria-current', 'step');
|
|
38
|
+
const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
|
|
39
|
+
mark.textContent = n < current ? '✓' : String(n);
|
|
40
|
+
const body = document.createElement('span');
|
|
41
|
+
body.textContent = text ?? '';
|
|
42
|
+
if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
|
|
43
|
+
li.append(mark, body);
|
|
44
|
+
return li;
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onAttributeChanged() { this._sync(); }
|
|
49
|
+
|
|
50
|
+
get steps() { return this._items(); }
|
|
51
|
+
set steps(v) { this._steps = Array.isArray(v) ? v : null; if (this._list) this._sync(); }
|
|
52
|
+
get current() { const n = parseInt(this.getAttribute('current'), 10); return Number.isFinite(n) ? n : 1; }
|
|
53
|
+
set current(v) { this.setAttribute('current', String(v)); }
|
|
54
|
+
/** Moves on to the next step; returns the new current step. */
|
|
55
|
+
next() { this.current = Math.min(this.current + 1, this._items().length + 1); return this.current; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
customElements.define('nk-steps', NkSteps);
|
|
59
|
+
export { NkSteps };
|