@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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)",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"import": "./dist/notionkit-elements.esm.js",
|
|
21
21
|
"default": "./dist/notionkit-elements.js"
|
|
22
22
|
},
|
|
23
|
+
"./base.js": "./dist/components/base.js",
|
|
23
24
|
"./components/*.js": "./dist/components/*.js",
|
|
24
25
|
"./package.json": "./package.json"
|
|
25
26
|
},
|
|
@@ -58,15 +59,15 @@
|
|
|
58
59
|
"dark-mode"
|
|
59
60
|
],
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"@jungherz-de/notionkit": ">=1.
|
|
62
|
+
"@jungherz-de/notionkit": ">=1.7.1"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
|
-
"@jungherz-de/notionkit": "^1.
|
|
65
|
+
"@jungherz-de/notionkit": "^1.7.1",
|
|
66
|
+
"@playwright/test": "^1.50.0",
|
|
65
67
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
66
68
|
"@rollup/plugin-terser": "^0.4.0",
|
|
67
|
-
"
|
|
68
|
-
"@playwright/test": "^1.50.0",
|
|
69
|
+
"pixelmatch": "^7.0.0",
|
|
69
70
|
"pngjs": "^7.0.0",
|
|
70
|
-
"
|
|
71
|
+
"rollup": "^4.0.0"
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
2
|
|
|
3
3
|
// <nk-progress value="72" label="72%"></nk-progress>
|
|
4
|
+
// <nk-progress value="72" label="72 %" wide></nk-progress> → fills its row, the label stays beside it
|
|
4
5
|
// The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
|
|
5
6
|
class NkProgress extends NkElement {
|
|
6
|
-
static get observedAttributes() { return ['value', 'max', 'label']; }
|
|
7
|
+
static get observedAttributes() { return ['value', 'max', 'label', 'wide']; }
|
|
7
8
|
|
|
8
9
|
render() {
|
|
9
10
|
this._bar = this.createElement('span', ['nk-progress'], { role: 'progressbar' });
|
|
@@ -22,6 +23,7 @@ class NkProgress extends NkElement {
|
|
|
22
23
|
const max = Number(this.getAttribute('max')) || 100;
|
|
23
24
|
const value = Math.min(max, Math.max(0, Number(this.getAttribute('value')) || 0));
|
|
24
25
|
this._fill.style.width = `${(value / max) * 100}%`;
|
|
26
|
+
this._bar.classList.toggle('wide', this.getBoolAttr('wide'));
|
|
25
27
|
this._bar.setAttribute('aria-valuenow', value);
|
|
26
28
|
this._bar.setAttribute('aria-valuemin', 0);
|
|
27
29
|
this._bar.setAttribute('aria-valuemax', max);
|
|
@@ -5,16 +5,18 @@ import { NkElement } from '../../base.js';
|
|
|
5
5
|
// Two tasks are open …
|
|
6
6
|
// <button slot="actions" value="copy">📋 Copy</button><button slot="actions" value="retry">↻ Rephrase</button>
|
|
7
7
|
// </nk-ai-msg>
|
|
8
|
+
// <nk-ai-msg role="user" bubble>What is still open?</nk-ai-msg> → a grey bubble on the right, as in Notion's AI chat
|
|
8
9
|
// → <div class="nk-ai-msg user"><span class="mini-avatar">MK</span><div class="a-body"><div class="a-name">You</div>…<div class="nk-ai-actions">…</div></div></div>
|
|
9
10
|
// Action buttons stay in the light DOM (slotted twins) and fire nk-action { action: value }.
|
|
10
11
|
class NkAiMsg extends NkElement {
|
|
11
|
-
static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color']; }
|
|
12
|
+
static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color', 'bubble']; }
|
|
12
13
|
|
|
13
14
|
render() {
|
|
14
15
|
this._box = this.createElement('div', ['nk-ai-msg']);
|
|
15
16
|
this._avatar = this.createElement('span', ['mini-avatar']);
|
|
16
17
|
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
17
18
|
avatarSlot.appendChild(this._avatar);
|
|
19
|
+
this._avatarSlot = avatarSlot;
|
|
18
20
|
const body = this.createElement('div', ['a-body']);
|
|
19
21
|
this._name = this.createElement('div', ['a-name']);
|
|
20
22
|
this._nameText = document.createTextNode('');
|
|
@@ -31,12 +33,16 @@ class NkAiMsg extends NkElement {
|
|
|
31
33
|
|
|
32
34
|
_sync() {
|
|
33
35
|
const user = this.getAttribute('role') === 'user';
|
|
36
|
+
const bubble = this.getBoolAttr('bubble');
|
|
34
37
|
this._box.classList.toggle('user', user);
|
|
38
|
+
this._box.classList.toggle('bubble', bubble);
|
|
35
39
|
const name = this.getAttribute('name');
|
|
36
40
|
this._nameText.data = name || '';
|
|
37
41
|
const badge = this.getAttribute('badge');
|
|
38
42
|
this._badge.textContent = badge ? ` ${badge}` : '';
|
|
39
|
-
|
|
43
|
+
// A bubble stands without avatar and name; the text says who asked.
|
|
44
|
+
this._name.style.display = name && !bubble ? '' : 'none';
|
|
45
|
+
this._avatarSlot.style.display = bubble ? 'none' : '';
|
|
40
46
|
this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
|
|
41
47
|
const color = this.getAttribute('color');
|
|
42
48
|
this._avatar.style.background = color || '';
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { paintAvatar, initialsOf } from '../../util/avatar.js';
|
|
2
3
|
|
|
3
|
-
// <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="
|
|
4
|
-
// → <div class="nk-comment"><span class="
|
|
4
|
+
// <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="green">The board view feels close.</nk-comment>
|
|
5
|
+
// → <div class="nk-comment"><span class="nk-avatar green">SL</span><div><div class="c-head"><b>Sara Lindt</b> · 1 hr ago</div><div class="c-body">…</div></div></div>
|
|
6
|
+
// color: one of Notion's nine names, or any CSS background; without it the avatar gradient.
|
|
5
7
|
// slot="head" adds content after the name (e.g. an <nk-tag>).
|
|
6
8
|
class NkComment extends NkElement {
|
|
7
9
|
static get observedAttributes() { return ['author', 'time', 'avatar', 'color']; }
|
|
8
10
|
|
|
9
11
|
render() {
|
|
10
12
|
const box = this.createElement('div', ['nk-comment']);
|
|
11
|
-
this._avatar = this.createElement('span', ['
|
|
13
|
+
this._avatar = this.createElement('span', ['nk-avatar']);
|
|
12
14
|
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
13
15
|
avatarSlot.appendChild(this._avatar);
|
|
14
16
|
const text = document.createElement('div');
|
|
@@ -29,8 +31,8 @@ class NkComment extends NkElement {
|
|
|
29
31
|
this._author.textContent = author;
|
|
30
32
|
const time = this.getAttribute('time');
|
|
31
33
|
this._time.data = time ? ` · ${time}` : '';
|
|
32
|
-
this._avatar.textContent = this.getAttribute('avatar') || author
|
|
33
|
-
this._avatar
|
|
34
|
+
this._avatar.textContent = this.getAttribute('avatar') || initialsOf(author);
|
|
35
|
+
paintAvatar(this._avatar, this.getAttribute('color'));
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
onAttributeChanged() { this._sync(); }
|
|
@@ -6,16 +6,24 @@ import { NkElement } from '../../base.js';
|
|
|
6
6
|
// </nk-database>
|
|
7
7
|
// db.columns = [{ key: 'name', label: 'Name', type: 'text', icon: '📄', title: true }, { key: 'status', type: 'select', options: [...] }, …];
|
|
8
8
|
// db.rows = [{ id: 1, icon: '🧭', name: '…', status: 'done', … }];
|
|
9
|
-
//
|
|
9
|
+
// <nk-btn slot="tools" variant="tool">Filter</nk-btn> … sit right of the tabs,
|
|
10
|
+
// <nk-filter-bar slot="filters"> under them (NotionKit 1.7.0's toolbar).
|
|
11
|
+
// → <div class="nk-database"><div class="nk-db-toolbar"><div class="nk-db-tabs">…</div>
|
|
12
|
+
// <div class="tools">…</div></div>…filters…active view…</div>
|
|
10
13
|
// The tabs come from the child views; data is pushed into every view.
|
|
11
14
|
class NkDatabase extends NkElement {
|
|
12
15
|
static get observedAttributes() { return ['view', 'add-view']; }
|
|
13
16
|
|
|
14
17
|
render() {
|
|
15
18
|
this._box = this.createElement('div', ['nk-database']);
|
|
19
|
+
this._toolbar = this.createElement('div', ['nk-db-toolbar']);
|
|
16
20
|
this._tabs = this.createElement('div', ['nk-db-tabs'], { role: 'tablist' });
|
|
21
|
+
const tools = this.createElement('div', ['tools']);
|
|
22
|
+
this._toolsSlot = this.createElement('slot', [], { name: 'tools' });
|
|
23
|
+
tools.appendChild(this._toolsSlot);
|
|
24
|
+
this._toolbar.append(this._tabs, tools);
|
|
17
25
|
this._slot = document.createElement('slot');
|
|
18
|
-
this._box.append(this.
|
|
26
|
+
this._box.append(this._toolbar, this.createElement('slot', [], { name: 'filters' }), this._slot);
|
|
19
27
|
this._wrapper.appendChild(this._box);
|
|
20
28
|
this._columns = []; this._rows = [];
|
|
21
29
|
this._sync();
|
|
@@ -43,12 +51,11 @@ class NkDatabase extends NkElement {
|
|
|
43
51
|
v.setData?.(this._columns, this._rows);
|
|
44
52
|
}
|
|
45
53
|
if (this.getBoolAttr('add-view')) {
|
|
46
|
-
const add = this.createElement('span', ['nk-db-tab'], { role: 'button', 'data-add': '', tabindex: '0' });
|
|
47
|
-
add.style.color = 'var(--nk-text-tertiary)';
|
|
54
|
+
const add = this.createElement('span', ['nk-db-tab', 'add'], { role: 'button', 'data-add': '', tabindex: '0' });
|
|
48
55
|
add.textContent = '+';
|
|
49
56
|
this._tabs.appendChild(add);
|
|
50
57
|
}
|
|
51
|
-
this.
|
|
58
|
+
this._toolbar.hidden = !views.length && !this._toolsSlot.assignedElements().length;
|
|
52
59
|
this._current = current;
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -70,6 +77,7 @@ class NkDatabase extends NkElement {
|
|
|
70
77
|
this._tabs.addEventListener('click', this._onClick);
|
|
71
78
|
this._tabs.addEventListener('keydown', this._onKey);
|
|
72
79
|
this._slot.addEventListener('slotchange', this._onSlot);
|
|
80
|
+
this._toolsSlot.addEventListener('slotchange', this._onSlot);
|
|
73
81
|
this._sync();
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -77,6 +85,7 @@ class NkDatabase extends NkElement {
|
|
|
77
85
|
this._tabs?.removeEventListener('click', this._onClick);
|
|
78
86
|
this._tabs?.removeEventListener('keydown', this._onKey);
|
|
79
87
|
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
88
|
+
this._toolsSlot?.removeEventListener('slotchange', this._onSlot);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
onAttributeChanged(name) {
|
|
@@ -1,60 +1,76 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
-
// <nk-filter-bar
|
|
4
|
-
// bar.filters = [{ key: 'status',
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// <nk-filter-bar add no-filter no-sort></nk-filter-bar>
|
|
4
|
+
// bar.filters = [{ key: 'status', value: 'done', op: 'is-not', label: 'Status: Open' }];
|
|
5
|
+
// The filters in effect as NotionKit's filter pills (1.7.0), no inline style:
|
|
6
|
+
// → <div class="nk-filter-row">[Filter][Sort]
|
|
7
|
+
// <span class="nk-filter-pill active"><button>Status: Open</button><button class="fp-remove">×</button></span>…
|
|
8
|
+
// <button class="nk-filter-pill add">+ Filter</button> …slot… [search]</div>
|
|
9
|
+
// Filter and Sort are .nk-db-tool buttons (hide them with no-filter / no-sort
|
|
10
|
+
// when the database toolbar carries its own); they fire nk-action. A pill's
|
|
11
|
+
// label fires nk-action { action: 'edit', index, filter }, the add pill
|
|
12
|
+
// nk-action { action: 'add' } – each with the clicked button as `anchor`, to
|
|
13
|
+
// open a menu under it. × removes the filter; every change to filters or
|
|
14
|
+
// search fires nk-change { filters, search }. In slot="filters" of
|
|
15
|
+
// <nk-database> the row sits right under the view tabs. The texts are
|
|
16
|
+
// attributes, for other languages: filter-label, sort-label, add-label,
|
|
17
|
+
// remove-label (the × reads "<remove-label>: <pill>") and placeholder.
|
|
8
18
|
class NkFilterBar extends NkElement {
|
|
9
|
-
static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value']; }
|
|
19
|
+
static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value', 'add', 'add-label', 'filter-label', 'sort-label', 'remove-label']; }
|
|
10
20
|
|
|
11
21
|
render() {
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this._sortBtn.textContent = '↕ Sort';
|
|
18
|
-
this._chips = document.createElement('span');
|
|
19
|
-
this._chips.style.cssText = 'display:inline-flex;gap:4px;flex-wrap:wrap';
|
|
22
|
+
this._row = this.createElement('div', ['nk-filter-row']);
|
|
23
|
+
this._filterBtn = this.createElement('button', ['nk-db-tool'], { type: 'button', 'data-action': 'filter' });
|
|
24
|
+
this._sortBtn = this.createElement('button', ['nk-db-tool'], { type: 'button', 'data-action': 'sort' });
|
|
25
|
+
this._pillStart = document.createComment('pills');
|
|
26
|
+
this._addBtn = this.createElement('button', ['nk-filter-pill', 'add'], { type: 'button', 'data-action': 'add' });
|
|
20
27
|
this._input = this.createElement('input', ['nk-input'], { type: 'search', autocomplete: 'off' });
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
23
|
-
this._wrapper.appendChild(this._bar);
|
|
28
|
+
this._row.append(this._filterBtn, this._sortBtn, this._pillStart, this._addBtn, document.createElement('slot'), this._input);
|
|
29
|
+
this._wrapper.appendChild(this._row);
|
|
24
30
|
this._filters = [];
|
|
25
31
|
this._sync();
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
_sync() {
|
|
29
|
-
this._filterBtn.
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
35
|
+
this._filterBtn.textContent = this.getAttribute('filter-label') || 'Filter';
|
|
36
|
+
this._filterBtn.hidden = this.getBoolAttr('no-filter');
|
|
37
|
+
this._filterBtn.classList.toggle('active', this._filters.length > 0);
|
|
38
|
+
this._sortBtn.textContent = this.getAttribute('sort-label') || 'Sort';
|
|
39
|
+
this._sortBtn.hidden = this.getBoolAttr('no-sort');
|
|
40
|
+
this._addBtn.textContent = this.getAttribute('add-label') || '+ Filter';
|
|
41
|
+
this._addBtn.hidden = !this.getBoolAttr('add');
|
|
42
|
+
this._input.hidden = !this.getBoolAttr('search');
|
|
32
43
|
this._input.placeholder = this.getAttribute('placeholder') || 'Search …';
|
|
33
44
|
if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
|
|
45
|
+
for (const old of this._row.querySelectorAll('[data-pill]')) old.remove();
|
|
46
|
+
const removeLabel = this.getAttribute('remove-label') || 'Remove filter';
|
|
47
|
+
this._pillStart.after(...this._filters.map((f, i) => {
|
|
48
|
+
const pill = this.createElement('span', ['nk-filter-pill', 'active'], { 'data-pill': i });
|
|
49
|
+
const label = this.createElement('button', [], { type: 'button', 'data-edit': i });
|
|
50
|
+
label.textContent = f.label ?? `${f.key}: ${f.value}`;
|
|
51
|
+
const remove = this.createElement('button', ['fp-remove'], { type: 'button', 'data-remove': i, 'aria-label': `${removeLabel}: ${label.textContent}` });
|
|
52
|
+
remove.textContent = '×';
|
|
53
|
+
pill.append(label, remove);
|
|
54
|
+
return pill;
|
|
55
|
+
}));
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
setupEvents() {
|
|
45
59
|
this._onClick = (e) => {
|
|
60
|
+
const remove = e.target.closest('[data-remove]');
|
|
61
|
+
if (remove) { this._filters.splice(Number(remove.dataset.remove), 1); this._sync(); this._emitChange(); return; }
|
|
62
|
+
const edit = e.target.closest('[data-edit]');
|
|
63
|
+
if (edit) { const index = Number(edit.dataset.edit); this.emit('nk-action', { action: 'edit', index, filter: this._filters[index], anchor: edit }); return; }
|
|
46
64
|
const btn = e.target.closest('[data-action]');
|
|
47
|
-
if (btn)
|
|
48
|
-
const chip = e.target.closest('[data-index]');
|
|
49
|
-
if (chip) { this._filters.splice(Number(chip.dataset.index), 1); this._sync(); this._emitChange(); }
|
|
65
|
+
if (btn) this.emit('nk-action', { action: btn.dataset.action, anchor: btn });
|
|
50
66
|
};
|
|
51
67
|
this._onInput = () => { clearTimeout(this._debounce); this._debounce = setTimeout(() => this._emitChange(), 150); };
|
|
52
|
-
this.
|
|
68
|
+
this._row.addEventListener('click', this._onClick);
|
|
53
69
|
this._input.addEventListener('input', this._onInput);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
teardownEvents() {
|
|
57
|
-
this.
|
|
73
|
+
this._row?.removeEventListener('click', this._onClick);
|
|
58
74
|
this._input?.removeEventListener('input', this._onInput);
|
|
59
75
|
clearTimeout(this._debounce);
|
|
60
76
|
}
|
|
@@ -63,13 +79,18 @@ class NkFilterBar extends NkElement {
|
|
|
63
79
|
onAttributeChanged() { this._sync(); }
|
|
64
80
|
|
|
65
81
|
get filters() { return this._filters; }
|
|
66
|
-
set filters(v) { this._filters = Array.isArray(v) ? v : []; this._sync(); }
|
|
82
|
+
set filters(v) { this._filters = Array.isArray(v) ? v : []; if (this._row) this._sync(); }
|
|
67
83
|
get value() { return this._input?.value ?? ''; }
|
|
68
84
|
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
69
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Applies filters and search to rows: a filter keeps rows where row[key]
|
|
87
|
+
* equals value – or differs from it with op: 'is-not'; the search looks
|
|
88
|
+
* for the text in every string field (a person's name).
|
|
89
|
+
*/
|
|
70
90
|
apply(rows) {
|
|
71
91
|
const q = this.value.trim().toLowerCase();
|
|
72
|
-
return rows.filter(r => this._filters.every(f => r[f.key]
|
|
92
|
+
return rows.filter(r => this._filters.every(f => (f.op === 'is-not' ? r[f.key] !== f.value : r[f.key] === f.value))
|
|
93
|
+
&& (!q || Object.values(r).some(v => String(typeof v === 'object' && v ? v.name ?? '' : v ?? '').toLowerCase().includes(q))));
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
import { renderPropertyCell } from '../../util/property-cell.js';
|
|
3
|
+
|
|
4
|
+
// <nk-list-view name="list" label="☰ List" meta-keys="due,status" new-row></nk-list-view>
|
|
5
|
+
// The third database view: one line per row – icon and title, the `meta-keys`
|
|
6
|
+
// on the right (default: the select and date columns, in column order). Dates
|
|
7
|
+
// and text stand as text, selects as tags, a person as avatar and name.
|
|
8
|
+
// Standalone: view.columns = […]; view.rows = […]. Inside <nk-database> the
|
|
9
|
+
// database pushes the data. Rows fire nk-select, the add row nk-action
|
|
10
|
+
// { action: 'new-row' }.
|
|
11
|
+
// → <div class="nk-list"><div class="nk-list-item"><span class="l-icon">…</span><span class="l-title">…</span><span class="l-meta">…</span></div>…</div>
|
|
12
|
+
class NkListView extends NkElement {
|
|
13
|
+
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'title-key', 'meta-keys', 'new-row', 'new-row-label']; }
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
this._box = document.createElement('div');
|
|
17
|
+
this._list = this.createElement('div', ['nk-list'], { role: 'list' });
|
|
18
|
+
this._newRow = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0' });
|
|
19
|
+
this._box.append(this._list, this._newRow);
|
|
20
|
+
this._wrapper.appendChild(this._box);
|
|
21
|
+
this._columns ??= []; this._rows ??= [];
|
|
22
|
+
this._render();
|
|
23
|
+
this.closest('nk-database')?.requestSync?.();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_titleColumn() {
|
|
27
|
+
const key = this.getAttribute('title-key');
|
|
28
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
|
|
29
|
+
}
|
|
30
|
+
_metaColumns() {
|
|
31
|
+
const keys = (this.getAttribute('meta-keys') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
32
|
+
if (keys.length) return keys.map(k => this._columns.find(c => c.key === k)).filter(Boolean);
|
|
33
|
+
return this._columns.filter(c => c.type === 'select' || c.type === 'date');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_render() {
|
|
37
|
+
if (!this._list) return;
|
|
38
|
+
const title = this._titleColumn(), meta = this._metaColumns();
|
|
39
|
+
this._list.replaceChildren();
|
|
40
|
+
for (const row of this._rows) {
|
|
41
|
+
const item = this.createElement('div', ['nk-list-item'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
|
|
42
|
+
const icon = this.createElement('span', ['l-icon']);
|
|
43
|
+
icon.textContent = row.icon || '';
|
|
44
|
+
if (!row.icon) icon.style.display = 'none';
|
|
45
|
+
const t = this.createElement('span', ['l-title']);
|
|
46
|
+
t.textContent = title ? String(row[title.key] ?? '') : '';
|
|
47
|
+
const m = this.createElement('span', ['l-meta']);
|
|
48
|
+
for (const c of meta) {
|
|
49
|
+
const v = row[c.key];
|
|
50
|
+
if (v === undefined || v === null || v === '') continue;
|
|
51
|
+
m.appendChild(c.type === 'date' || c.type === 'text' || !c.type ? document.createTextNode(String(v)) : renderPropertyCell(c, v, row));
|
|
52
|
+
}
|
|
53
|
+
item.append(icon, t, m);
|
|
54
|
+
this._list.appendChild(item);
|
|
55
|
+
}
|
|
56
|
+
this._newRow.textContent = this.getAttribute('new-row-label') || '+ New page';
|
|
57
|
+
this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setupEvents() {
|
|
61
|
+
this._onClick = (e) => {
|
|
62
|
+
if (this._newRow.contains(e.target)) { this.emit('nk-action', { action: 'new-row' }); return; }
|
|
63
|
+
const item = e.target.closest('.nk-list-item');
|
|
64
|
+
if (item) { const row = this._rowById(item.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
|
|
65
|
+
};
|
|
66
|
+
this._onKey = (e) => {
|
|
67
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
68
|
+
if (e.target === this._newRow || e.target.classList?.contains('nk-list-item')) { e.preventDefault(); e.target.click(); }
|
|
69
|
+
};
|
|
70
|
+
this._box.addEventListener('click', this._onClick);
|
|
71
|
+
this._box.addEventListener('keydown', this._onKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
teardownEvents() {
|
|
75
|
+
this._box?.removeEventListener('click', this._onClick);
|
|
76
|
+
this._box?.removeEventListener('keydown', this._onKey);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
80
|
+
onAttributeChanged() { this._render(); }
|
|
81
|
+
|
|
82
|
+
get name() { return this.getAttribute('name') || 'list'; }
|
|
83
|
+
get columns() { return this._columns; }
|
|
84
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
|
|
85
|
+
get rows() { return this._rows; }
|
|
86
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
|
|
87
|
+
set data({ columns, rows }) { this.setData(columns, rows); }
|
|
88
|
+
setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
|
|
89
|
+
refresh() { this._render(); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
customElements.define('nk-list-view', NkListView);
|
|
93
|
+
export { NkListView };
|
|
@@ -50,6 +50,8 @@ class NkTableView extends NkElement {
|
|
|
50
50
|
const r = this.createElement('tr', [], { 'data-id': row.id ?? '' });
|
|
51
51
|
for (const col of this._columns) {
|
|
52
52
|
const td = document.createElement('td');
|
|
53
|
+
// Numbers right-aligned in figures of equal width, as Notion sets them.
|
|
54
|
+
if (col.type === 'number') td.className = 'num';
|
|
53
55
|
td.appendChild(renderPropertyCell(col, row[col.key], row));
|
|
54
56
|
r.appendChild(td);
|
|
55
57
|
}
|
|
@@ -3,13 +3,16 @@ import { NkElement } from '../../base.js';
|
|
|
3
3
|
// <nk-btn variant="primary" small>Save</nk-btn> → <button class="nk-btn primary small">
|
|
4
4
|
// <nk-btn href="/docs" variant="secondary">Docs</nk-btn> → <a class="nk-btn secondary">
|
|
5
5
|
// <nk-btn variant="topbar">⭐</nk-btn> → <button class="nk-topbar-btn">
|
|
6
|
+
// <nk-btn variant="tool" active>Filter</nk-btn> → <button class="nk-db-tool active"> (slot="tools" of nk-database)
|
|
7
|
+
// <nk-btn variant="sidebar" aria-label="Menu">☰</nk-btn> → <button class="nk-topbar-btn nk-sidebar-toggle">:
|
|
8
|
+
// shown on phones only, it opens the page's <nk-sidebar> as a drawer.
|
|
6
9
|
//
|
|
7
10
|
// Modifier classes become attributes. A slotted <svg> is sized by the
|
|
8
11
|
// `.nk-btn ::slotted(svg)` twin; pass the icon itself, never wrapped.
|
|
9
12
|
const VARIANTS = ['primary', 'secondary', 'danger', 'danger-solid', 'topbar', 'share'];
|
|
10
13
|
|
|
11
14
|
class NkBtn extends NkElement {
|
|
12
|
-
static get observedAttributes() { return ['variant', 'small', 'disabled', 'type', 'href', 'title', 'aria-label']; }
|
|
15
|
+
static get observedAttributes() { return ['variant', 'small', 'disabled', 'type', 'href', 'title', 'aria-label', 'aria-haspopup', 'active']; }
|
|
13
16
|
|
|
14
17
|
render() {
|
|
15
18
|
this._build();
|
|
@@ -37,13 +40,22 @@ class NkBtn extends NkElement {
|
|
|
37
40
|
else el.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
|
38
41
|
const title = this.getAttribute('title');
|
|
39
42
|
title ? el.setAttribute('title', title) : el.removeAttribute('title');
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// The accessible name and the popup hint belong on the button itself.
|
|
44
|
+
for (const name of ['aria-label', 'aria-haspopup']) {
|
|
45
|
+
const v = this.getAttribute(name);
|
|
46
|
+
v ? el.setAttribute(name, v) : el.removeAttribute(name);
|
|
47
|
+
}
|
|
48
|
+
if (this.getAttribute('variant') === 'sidebar') el.setAttribute('aria-expanded', this._sidebar()?.open ? 'true' : 'false');
|
|
49
|
+
else el.removeAttribute('aria-expanded');
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/** The sidebar the ☰ opens: the one in its own app, else the page's. */
|
|
53
|
+
_sidebar() { return this.closest('nk-app')?.querySelector('nk-sidebar') ?? document.querySelector('nk-sidebar'); }
|
|
54
|
+
|
|
44
55
|
setupEvents() {
|
|
45
56
|
this._onClick = (e) => {
|
|
46
57
|
if (this.getBoolAttr('disabled')) { e.preventDefault(); e.stopPropagation(); return; }
|
|
58
|
+
if (this.getAttribute('variant') === 'sidebar') { this._sidebar()?.toggle?.(); return; }
|
|
47
59
|
// A button inside a shadow root is not a submit button of the outer
|
|
48
60
|
// form; forward the intent to the form the host sits in.
|
|
49
61
|
const type = this.getAttribute('type');
|
|
@@ -52,11 +64,17 @@ class NkBtn extends NkElement {
|
|
|
52
64
|
if (form) type === 'submit' ? form.requestSubmit() : form.reset();
|
|
53
65
|
}
|
|
54
66
|
};
|
|
67
|
+
// The ☰ says whether its drawer is open, however the drawer closed.
|
|
68
|
+
this._onToggle = (e) => {
|
|
69
|
+
if (this.getAttribute('variant') === 'sidebar' && e.target === this._sidebar()) this._btn.setAttribute('aria-expanded', e.detail.open ? 'true' : 'false');
|
|
70
|
+
};
|
|
55
71
|
this._btn.addEventListener('click', this._onClick);
|
|
72
|
+
document.addEventListener('nk-toggle', this._onToggle);
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
teardownEvents() {
|
|
59
76
|
this._btn?.removeEventListener('click', this._onClick);
|
|
77
|
+
document.removeEventListener('nk-toggle', this._onToggle);
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
onAttributeChanged(name) {
|
|
@@ -66,7 +84,11 @@ class NkBtn extends NkElement {
|
|
|
66
84
|
this._build();
|
|
67
85
|
break;
|
|
68
86
|
case 'variant':
|
|
87
|
+
this._btn.className = this._computeClasses().join(' ');
|
|
88
|
+
this._applyState(this._btn);
|
|
89
|
+
break;
|
|
69
90
|
case 'small':
|
|
91
|
+
case 'active':
|
|
70
92
|
this._btn.className = this._computeClasses().join(' ');
|
|
71
93
|
break;
|
|
72
94
|
case 'type':
|
|
@@ -81,6 +103,8 @@ class NkBtn extends NkElement {
|
|
|
81
103
|
const variant = this.getAttribute('variant');
|
|
82
104
|
if (variant === 'topbar') return ['nk-topbar-btn'];
|
|
83
105
|
if (variant === 'share') return ['nk-topbar-btn', 'nk-share-btn'];
|
|
106
|
+
if (variant === 'sidebar') return ['nk-topbar-btn', 'nk-sidebar-toggle'];
|
|
107
|
+
if (variant === 'tool') return this.getBoolAttr('active') ? ['nk-db-tool', 'active'] : ['nk-db-tool'];
|
|
84
108
|
const classes = ['nk-btn'];
|
|
85
109
|
if (VARIANTS.includes(variant)) classes.push(variant);
|
|
86
110
|
if (this.getBoolAttr('small')) classes.push('small');
|
|
@@ -92,6 +116,8 @@ class NkBtn extends NkElement {
|
|
|
92
116
|
|
|
93
117
|
get variant() { return this.getAttribute('variant'); }
|
|
94
118
|
set variant(v) { v ? this.setAttribute('variant', v) : this.removeAttribute('variant'); }
|
|
119
|
+
get active() { return this.getBoolAttr('active'); }
|
|
120
|
+
set active(v) { this.setBoolAttr('active', v); }
|
|
95
121
|
get small() { return this.getBoolAttr('small'); }
|
|
96
122
|
set small(v) { this.setBoolAttr('small', v); }
|
|
97
123
|
get disabled() { return this.getBoolAttr('disabled'); }
|
|
@@ -3,9 +3,15 @@ import { NkElement } from '../../base.js';
|
|
|
3
3
|
// <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
|
|
4
4
|
// <nk-menu-item type="separator"></nk-menu-item> <nk-menu-item type="label">Danger</nk-menu-item>
|
|
5
5
|
// <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
|
|
6
|
+
// <nk-menu-item type="switch" icon="🔡" value="small" checked>Small text</nk-menu-item>
|
|
7
|
+
// <nk-menu-item type="check" icon="◉" value="open" checked>Status: Open</nk-menu-item>
|
|
6
8
|
// → <div class="nk-menu-item danger"><span class="m-icon">🗑️</span>Delete<span class="m-shortcut">…</span></div>
|
|
9
|
+
// A switch item carries a .nk-switch on the right, as "Small text" does in
|
|
10
|
+
// Notion's page menu; a check item a ✓ where the shortcut stands, as in a
|
|
11
|
+
// filter menu. A click on either flips `checked` and fires nk-change
|
|
12
|
+
// { value, checked } instead of nk-select, so the menu around it stays open.
|
|
7
13
|
class NkMenuItem extends NkElement {
|
|
8
|
-
static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled']; }
|
|
14
|
+
static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled', 'checked']; }
|
|
9
15
|
|
|
10
16
|
render() { this._build(); }
|
|
11
17
|
|
|
@@ -18,12 +24,16 @@ class NkMenuItem extends NkElement {
|
|
|
18
24
|
el = this.createElement('div', ['nk-menu-label']);
|
|
19
25
|
el.appendChild(document.createElement('slot'));
|
|
20
26
|
} else {
|
|
21
|
-
|
|
27
|
+
const isSwitch = type === 'switch';
|
|
28
|
+
this._check = type === 'check';
|
|
29
|
+
el = this.createElement('div', ['nk-menu-item'], { role: isSwitch || this._check ? 'menuitemcheckbox' : 'menuitem', tabindex: '-1' });
|
|
22
30
|
this._icon = this.createElement('span', ['m-icon']);
|
|
23
31
|
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
24
32
|
iconSlot.appendChild(this._icon);
|
|
25
33
|
this._shortcut = this.createElement('span', ['m-shortcut']);
|
|
26
34
|
el.append(iconSlot, document.createElement('slot'), this._shortcut);
|
|
35
|
+
this._switch = isSwitch ? this.createElement('span', ['nk-switch'], { 'aria-hidden': 'true' }) : null;
|
|
36
|
+
if (this._switch) el.appendChild(this._switch);
|
|
27
37
|
}
|
|
28
38
|
if (this._el) { this._el.removeEventListener('click', this._onClick); this._el.replaceWith(el); } else this._wrapper.appendChild(el);
|
|
29
39
|
this._el = el;
|
|
@@ -41,6 +51,12 @@ class NkMenuItem extends NkElement {
|
|
|
41
51
|
this._shortcut.style.display = sc ? '' : 'none';
|
|
42
52
|
this._el.classList.toggle('danger', this.getBoolAttr('danger'));
|
|
43
53
|
this._el.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
|
|
54
|
+
if (this._switch || this._check) {
|
|
55
|
+
const on = this.getBoolAttr('checked') ? 'true' : 'false';
|
|
56
|
+
this._el.setAttribute('aria-checked', on);
|
|
57
|
+
if (this._switch) this._switch.setAttribute('aria-checked', on);
|
|
58
|
+
if (this._check) { this._shortcut.textContent = on === 'true' ? '✓' : ''; this._shortcut.style.display = ''; }
|
|
59
|
+
}
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
setupEvents() {
|
|
@@ -52,6 +68,11 @@ class NkMenuItem extends NkElement {
|
|
|
52
68
|
|
|
53
69
|
select() {
|
|
54
70
|
if (!this._el.classList.contains('nk-menu-item') || this.getBoolAttr('disabled')) return;
|
|
71
|
+
if (this._switch || this._check) {
|
|
72
|
+
this.checked = !this.checked;
|
|
73
|
+
this.emit('nk-change', { value: this.value, checked: this.checked, item: this });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
55
76
|
this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -59,6 +80,8 @@ class NkMenuItem extends NkElement {
|
|
|
59
80
|
focus(o) { this._el?.focus(o); }
|
|
60
81
|
get selectable() { return this._el?.classList.contains('nk-menu-item') && !this.getBoolAttr('disabled'); }
|
|
61
82
|
|
|
83
|
+
get checked() { return this.getBoolAttr('checked'); }
|
|
84
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
62
85
|
get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
|
|
63
86
|
set value(v) { this.setAttribute('value', v); }
|
|
64
87
|
}
|