@jungherz-de/notionkit-elements 1.16.0 → 1.18.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/CHANGELOG.md +32 -0
- package/README.md +64 -6
- package/SKILL.md +79 -65
- package/dist/components/base.js +102 -3
- package/dist/components/nk-ai-input-row.js +6 -3
- package/dist/components/nk-board-view.js +2 -2
- package/dist/components/nk-breadcrumb.js +2 -1
- package/dist/components/nk-calendar-view.js +8 -5
- package/dist/components/nk-calendar.js +10 -7
- package/dist/components/nk-cmdk.js +6 -2
- package/dist/components/nk-comments.js +5 -3
- package/dist/components/nk-copy-field.js +37 -6
- package/dist/components/nk-emoji-picker.js +6 -2
- package/dist/components/nk-filter-bar.js +10 -8
- package/dist/components/nk-gallery-view.js +29 -7
- package/dist/components/nk-image-picker.js +7 -3
- package/dist/components/nk-list-view.js +3 -2
- package/dist/components/nk-modal.js +3 -1
- package/dist/components/nk-page-title.js +2 -1
- package/dist/components/nk-page.js +3 -1
- package/dist/components/nk-peek.js +6 -3
- package/dist/components/nk-progress.js +6 -3
- package/dist/components/nk-sidebar.js +5 -2
- package/dist/components/nk-steps.js +20 -4
- package/dist/components/nk-synced.js +2 -1
- package/dist/components/nk-tab-bar.js +10 -4
- package/dist/components/nk-table-view.js +3 -2
- package/dist/components/nk-theme-toggle.js +5 -2
- package/dist/components/nk-tree-item.js +3 -1
- package/dist/components/shared/{property-cell-BSVcETss.js → property-cell-DcsfnUNF.js} +15 -5
- package/dist/notionkit-elements.esm.js +312 -80
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +314 -79
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +32 -2
- package/src/components/content/nk-progress.js +6 -3
- package/src/components/data/nk-ai-input-row.js +6 -3
- package/src/components/data/nk-board-view.js +2 -2
- package/src/components/data/nk-calendar-view.js +7 -4
- package/src/components/data/nk-comments.js +5 -3
- package/src/components/data/nk-filter-bar.js +10 -8
- package/src/components/data/nk-gallery-view.js +29 -7
- package/src/components/data/nk-list-view.js +2 -1
- package/src/components/data/nk-table-view.js +2 -1
- package/src/components/forms/nk-calendar.js +10 -7
- package/src/components/forms/nk-copy-field.js +37 -6
- package/src/components/forms/nk-image-picker.js +7 -3
- package/src/components/overlays/nk-cmdk.js +6 -2
- package/src/components/overlays/nk-emoji-picker.js +6 -2
- package/src/components/overlays/nk-modal.js +3 -1
- package/src/components/overlays/nk-peek.js +6 -3
- package/src/components/page/nk-page-title.js +2 -1
- package/src/components/page/nk-page.js +3 -1
- package/src/components/page/nk-steps.js +20 -4
- package/src/components/page/nk-synced.js +2 -1
- package/src/components/shell/nk-breadcrumb.js +2 -1
- package/src/components/shell/nk-sidebar.js +5 -2
- package/src/components/shell/nk-tab-bar.js +10 -4
- package/src/components/shell/nk-theme-toggle.js +5 -2
- package/src/components/shell/nk-tree-item.js +3 -1
- package/src/index.js +5 -1
- package/src/util/property-cell.js +14 -4
- package/src/util/strings.js +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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)",
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"dark-mode"
|
|
64
64
|
],
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@jungherz-de/notionkit": ">=1.
|
|
66
|
+
"@jungherz-de/notionkit": ">=1.18.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@jungherz-de/notionkit": "^1.
|
|
69
|
+
"@jungherz-de/notionkit": "^1.18.0",
|
|
70
70
|
"@playwright/test": "^1.50.0",
|
|
71
71
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
72
72
|
"@rollup/plugin-terser": "^0.4.0",
|
package/src/base.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// nothing else; the element ships no visual CSS of its own.
|
|
7
7
|
// ============================================================
|
|
8
8
|
import { componentsSheet, tokensCss } from '@jungherz-de/notionkit/notionkit-styles.js';
|
|
9
|
+
import { onStringsChange, str as textFor } from './util/strings.js';
|
|
9
10
|
|
|
10
11
|
// ── Design tokens ──
|
|
11
12
|
// The shadow roots deliberately adopt componentsSheet only. The full sheet
|
|
@@ -49,10 +50,20 @@ function syncAllThemes() {
|
|
|
49
50
|
for (const instance of instances) instance._syncTheme(theme);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// The built-in texts follow the language (1.17.0): setStrings() and a new
|
|
54
|
+
// lang on <html> reach the elements already on the page, too.
|
|
55
|
+
function syncAllStrings() {
|
|
56
|
+
for (const instance of instances) instance.onStringsChanged();
|
|
57
|
+
}
|
|
58
|
+
onStringsChange(syncAllStrings);
|
|
59
|
+
|
|
52
60
|
if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
|
|
53
|
-
new MutationObserver(
|
|
61
|
+
new MutationObserver(records => {
|
|
62
|
+
if (records.some(r => r.attributeName === 'data-theme')) syncAllThemes();
|
|
63
|
+
if (records.some(r => r.attributeName === 'lang')) syncAllStrings();
|
|
64
|
+
}).observe(document.documentElement, {
|
|
54
65
|
attributes: true,
|
|
55
|
-
attributeFilter: ['data-theme']
|
|
66
|
+
attributeFilter: ['data-theme', 'lang']
|
|
56
67
|
});
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -197,6 +208,21 @@ export class NkElement extends HTMLElement {
|
|
|
197
208
|
this._wrapper?.setAttribute('data-theme', theme);
|
|
198
209
|
}
|
|
199
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The text `key` in this element's language – its nearest lang attribute,
|
|
213
|
+
* across shadow roots, else the page's: the one setStrings() gave, else the
|
|
214
|
+
* built-in English or German one (1.17.0). A project's own element reads
|
|
215
|
+
* its texts here too, after setStrings({ myKey: '…' }).
|
|
216
|
+
*/
|
|
217
|
+
str(key) { return textFor(key, this); }
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The texts may have changed: setStrings() ran or <html lang> changed.
|
|
221
|
+
* Components with built-in texts apply them again here; an attribute on
|
|
222
|
+
* the element still wins.
|
|
223
|
+
*/
|
|
224
|
+
onStringsChanged() {}
|
|
225
|
+
|
|
200
226
|
_upgradeOwnProperties() {
|
|
201
227
|
for (const key of Object.keys(this)) {
|
|
202
228
|
if (key.startsWith('_')) continue;
|
|
@@ -347,3 +373,7 @@ export class NkFormElement extends NkElement {
|
|
|
347
373
|
}
|
|
348
374
|
|
|
349
375
|
export { getCurrentTheme };
|
|
376
|
+
|
|
377
|
+
// For projects that import single elements (components/nk-*.js): the one
|
|
378
|
+
// dictionary every element reads its built-in texts from.
|
|
379
|
+
export { setStrings, builtInStrings } from './util/strings.js';
|
|
@@ -3,6 +3,9 @@ import { NkElement } from '../../base.js';
|
|
|
3
3
|
// <nk-progress value="72" label="72%"></nk-progress>
|
|
4
4
|
// <nk-progress value="72" label="72 %" wide></nk-progress> → fills its row, the label stays beside it
|
|
5
5
|
// The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
|
|
6
|
+
// With a label the two form one .nk-progress-row (1.18.0): the label beside
|
|
7
|
+
// the bar in a flex column too – a panel – where the host, display: contents,
|
|
8
|
+
// would let them stand one under the other.
|
|
6
9
|
class NkProgress extends NkElement {
|
|
7
10
|
static get observedAttributes() { return ['value', 'max', 'label', 'wide']; }
|
|
8
11
|
|
|
@@ -11,8 +14,7 @@ class NkProgress extends NkElement {
|
|
|
11
14
|
this._fill = document.createElement('i');
|
|
12
15
|
this._bar.appendChild(this._fill);
|
|
13
16
|
this._label = this.createElement('span', ['nk-progress-label']);
|
|
14
|
-
this.
|
|
15
|
-
this._wrapper.appendChild(this._label);
|
|
17
|
+
this._row = this.createElement('span', ['nk-progress-row']);
|
|
16
18
|
this._sync();
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -29,7 +31,8 @@ class NkProgress extends NkElement {
|
|
|
29
31
|
this._bar.setAttribute('aria-valuemax', max);
|
|
30
32
|
const label = this.getAttribute('label');
|
|
31
33
|
this._label.textContent = label ?? '';
|
|
32
|
-
|
|
34
|
+
if (label === null) { if (this._bar.parentNode !== this._wrapper) this._wrapper.replaceChildren(this._bar); }
|
|
35
|
+
else if (this._bar.parentNode !== this._row) { this._row.replaceChildren(this._bar, this._label); this._wrapper.replaceChildren(this._row); }
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
get value() { return Number(this.getAttribute('value')) || 0; }
|
|
@@ -10,8 +10,8 @@ class NkAiInputRow extends NkElement {
|
|
|
10
10
|
this._row = this.createElement('div', ['nk-ai-input-row']);
|
|
11
11
|
this._icon = document.createElement('span');
|
|
12
12
|
this._icon.style.fontSize = '14px';
|
|
13
|
-
this._input = this.createElement('input', [], { type: 'text'
|
|
14
|
-
this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button'
|
|
13
|
+
this._input = this.createElement('input', [], { type: 'text' });
|
|
14
|
+
this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button' });
|
|
15
15
|
this._btn.textContent = '↑';
|
|
16
16
|
this._row.append(this._icon, this._input, this._btn);
|
|
17
17
|
this._wrapper.appendChild(this._row);
|
|
@@ -19,7 +19,9 @@ class NkAiInputRow extends NkElement {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
_sync() {
|
|
22
|
-
this._input.placeholder = this.getAttribute('placeholder') || '
|
|
22
|
+
this._input.placeholder = this.getAttribute('placeholder') || this.str('askPlaceholder');
|
|
23
|
+
this._input.setAttribute('aria-label', this.str('message'));
|
|
24
|
+
this._btn.setAttribute('aria-label', this.str('send'));
|
|
23
25
|
if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
|
|
24
26
|
this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
|
|
25
27
|
const icon = this.getAttribute('icon') ?? '✨';
|
|
@@ -47,6 +49,7 @@ class NkAiInputRow extends NkElement {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
onAttributeChanged() { this._sync(); }
|
|
52
|
+
onStringsChanged() { this._sync(); }
|
|
50
53
|
focus(o) { this._input?.focus(o); }
|
|
51
54
|
get value() { return this._input?.value ?? ''; }
|
|
52
55
|
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
-
import { renderPropertyCell, tagFor, textOf, formatDate } from '../../util/property-cell.js';
|
|
2
|
+
import { renderPropertyCell, tagFor, textOf, formatDate, formatPercent } from '../../util/property-cell.js';
|
|
3
3
|
|
|
4
4
|
// <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
|
|
5
5
|
// Groups rows by a select column (default: the first select column); one
|
|
@@ -55,7 +55,7 @@ class NkBoardView extends NkElement {
|
|
|
55
55
|
const v = row[c.key];
|
|
56
56
|
if (v === undefined || v === null || v === '') continue;
|
|
57
57
|
const s = document.createElement('span');
|
|
58
|
-
if (c.type === 'progress') s.textContent = `▰ ${v}
|
|
58
|
+
if (c.type === 'progress') s.textContent = `▰ ${formatPercent(c, v)}`;
|
|
59
59
|
else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
|
|
60
60
|
else s.appendChild(renderPropertyCell(c, v, row));
|
|
61
61
|
meta.appendChild(s);
|
|
@@ -10,7 +10,8 @@ import { isoOf, parseDay, isoWeek, resolveLocale, firstWeekday, weekdayNames, mo
|
|
|
10
10
|
// default today's), weeks (the ISO week in front of each row), week-start
|
|
11
11
|
// (0 = Sunday … 6 = Saturday; default from the locale), weekend (weekdays
|
|
12
12
|
// washed like the days of other months, "6,0"), today, locale and the texts
|
|
13
|
-
// today-label, prev-label, next-label, week-label
|
|
13
|
+
// today-label, prev-label, next-label, week-label – without them English
|
|
14
|
+
// or German after the language (1.17.0).
|
|
14
15
|
// Standalone: view.columns = […]; view.rows = […]. Inside <nk-database> the
|
|
15
16
|
// database pushes the data. A card fires nk-select { row, id, value } like a
|
|
16
17
|
// row of the table, Today and ‹ › nk-month { month }.
|
|
@@ -66,9 +67,9 @@ class NkCalendarView extends NkElement {
|
|
|
66
67
|
}
|
|
67
68
|
this._box.classList.toggle('weeks', weeks);
|
|
68
69
|
this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
|
|
69
|
-
this._todayBtn.textContent = this.getAttribute('today-label') || '
|
|
70
|
-
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || '
|
|
71
|
-
this._next.setAttribute('aria-label', this.getAttribute('next-label') || '
|
|
70
|
+
this._todayBtn.textContent = this.getAttribute('today-label') || this.str('today');
|
|
71
|
+
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || this.str('previousMonth'));
|
|
72
|
+
this._next.setAttribute('aria-label', this.getAttribute('next-label') || this.str('nextMonth'));
|
|
72
73
|
|
|
73
74
|
const cell = (cls, text) => { const el = this.createElement('div', [cls]); el.textContent = text; return el; };
|
|
74
75
|
const nodes = [];
|
|
@@ -114,6 +115,8 @@ class NkCalendarView extends NkElement {
|
|
|
114
115
|
|
|
115
116
|
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
116
117
|
|
|
118
|
+
onStringsChanged() { this._render(); }
|
|
119
|
+
|
|
117
120
|
onAttributeChanged(name) {
|
|
118
121
|
if (name === 'month') this._month = this._attrMonth() ?? this._month;
|
|
119
122
|
this._render();
|
|
@@ -12,7 +12,7 @@ class NkComments extends NkElement {
|
|
|
12
12
|
const box = this.createElement('div', ['nk-comments']);
|
|
13
13
|
box.appendChild(document.createElement('slot'));
|
|
14
14
|
this._row = this.createElement('div', ['nk-comment-input']);
|
|
15
|
-
this._input = this.createElement('input', ['nk-input'], { type: 'text'
|
|
15
|
+
this._input = this.createElement('input', ['nk-input'], { type: 'text' });
|
|
16
16
|
this._btn = this.createElement('button', ['nk-btn', 'primary', 'small'], { type: 'button' });
|
|
17
17
|
this._row.append(this._input, this._btn);
|
|
18
18
|
box.appendChild(this._row);
|
|
@@ -21,8 +21,9 @@ class NkComments extends NkElement {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
_sync() {
|
|
24
|
-
this._input.placeholder = this.getAttribute('placeholder') || '
|
|
25
|
-
this.
|
|
24
|
+
this._input.placeholder = this.getAttribute('placeholder') || this.str('commentPlaceholder');
|
|
25
|
+
this._input.setAttribute('aria-label', this.str('comment'));
|
|
26
|
+
this._btn.textContent = this.getAttribute('send-label') || this.str('send');
|
|
26
27
|
this._row.style.display = this.getBoolAttr('no-input') ? 'none' : '';
|
|
27
28
|
this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
|
|
28
29
|
}
|
|
@@ -47,6 +48,7 @@ class NkComments extends NkElement {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
onAttributeChanged() { this._sync(); }
|
|
51
|
+
onStringsChanged() { this._sync(); }
|
|
50
52
|
focus(o) { this._input?.focus(o); }
|
|
51
53
|
get value() { return this._input?.value ?? ''; }
|
|
52
54
|
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
@@ -12,9 +12,10 @@ import { NkElement } from '../../base.js';
|
|
|
12
12
|
// nk-action { action: 'add' } – each with the clicked button as `anchor`, to
|
|
13
13
|
// open a menu under it. × removes the filter; every change to filters or
|
|
14
14
|
// search fires nk-change { filters, search }. In slot="filters" of
|
|
15
|
-
// <nk-database> the row sits right under the view tabs. The texts
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// <nk-database> the row sits right under the view tabs. The texts come in
|
|
16
|
+
// English or German after the language (1.17.0, setStrings() for others);
|
|
17
|
+
// filter-label, sort-label, add-label, remove-label (the × reads
|
|
18
|
+
// "<remove-label>: <pill>") and placeholder set them here.
|
|
18
19
|
class NkFilterBar extends NkElement {
|
|
19
20
|
static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value', 'add', 'add-label', 'filter-label', 'sort-label', 'remove-label']; }
|
|
20
21
|
|
|
@@ -32,18 +33,18 @@ class NkFilterBar extends NkElement {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
_sync() {
|
|
35
|
-
this._filterBtn.textContent = this.getAttribute('filter-label') || '
|
|
36
|
+
this._filterBtn.textContent = this.getAttribute('filter-label') || this.str('filter');
|
|
36
37
|
this._filterBtn.hidden = this.getBoolAttr('no-filter');
|
|
37
38
|
this._filterBtn.classList.toggle('active', this._filters.length > 0);
|
|
38
|
-
this._sortBtn.textContent = this.getAttribute('sort-label') || '
|
|
39
|
+
this._sortBtn.textContent = this.getAttribute('sort-label') || this.str('sort');
|
|
39
40
|
this._sortBtn.hidden = this.getBoolAttr('no-sort');
|
|
40
|
-
this._addBtn.textContent = this.getAttribute('add-label') || '
|
|
41
|
+
this._addBtn.textContent = this.getAttribute('add-label') || this.str('addFilter');
|
|
41
42
|
this._addBtn.hidden = !this.getBoolAttr('add');
|
|
42
43
|
this._input.hidden = !this.getBoolAttr('search');
|
|
43
|
-
this._input.placeholder = this.getAttribute('placeholder') || '
|
|
44
|
+
this._input.placeholder = this.getAttribute('placeholder') || this.str('search');
|
|
44
45
|
if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
|
|
45
46
|
for (const old of this._row.querySelectorAll('[data-pill]')) old.remove();
|
|
46
|
-
const removeLabel = this.getAttribute('remove-label') || '
|
|
47
|
+
const removeLabel = this.getAttribute('remove-label') || this.str('removeFilter');
|
|
47
48
|
this._pillStart.after(...this._filters.map((f, i) => {
|
|
48
49
|
const pill = this.createElement('span', ['nk-filter-pill', 'active'], { 'data-pill': i });
|
|
49
50
|
const label = this.createElement('button', [], { type: 'button', 'data-edit': i });
|
|
@@ -77,6 +78,7 @@ class NkFilterBar extends NkElement {
|
|
|
77
78
|
|
|
78
79
|
_emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
|
|
79
80
|
onAttributeChanged() { this._sync(); }
|
|
81
|
+
onStringsChanged() { this._sync(); }
|
|
80
82
|
|
|
81
83
|
// A copy each way: the caller's array is never changed – removing a pill
|
|
82
84
|
// only reports the new list in nk-change.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
-
import { renderPropertyCell, textOf, formatDate } from '../../util/property-cell.js';
|
|
2
|
+
import { renderPropertyCell, textOf, formatDate, formatPercent } from '../../util/property-cell.js';
|
|
3
3
|
|
|
4
4
|
// <nk-gallery-view name="gallery" label="🖼 Gallery" meta-keys="status,due" new-row></nk-gallery-view>
|
|
5
5
|
// The fifth database view (NotionKit 1.14.0): the rows as cards with a
|
|
@@ -13,13 +13,20 @@ import { renderPropertyCell, textOf, formatDate } from '../../util/property-cell
|
|
|
13
13
|
// <nk-database> the database pushes the data. A card fires nk-select
|
|
14
14
|
// { row, id, value } on a click, Enter or Space, like a row of the table;
|
|
15
15
|
// the add card nk-action { action: 'new-row' }.
|
|
16
|
+
// `href-key` (1.17.0) names the row field with the card's address: a row
|
|
17
|
+
// with one gets a link, <a class="nk-card" href>, in a .card-item that
|
|
18
|
+
// carries the list item's role – to open a course in a new tab or copy its
|
|
19
|
+
// address. A plain click, or Enter, fires the same nk-select; cancelling it
|
|
20
|
+
// keeps the browser from following the link. A middle click or one with
|
|
21
|
+
// Cmd, Ctrl, Shift or Alt fires nothing and does what a link does.
|
|
16
22
|
// → <div class="nk-gallery" role="list"><div class="nk-card" role="listitem" tabindex="0">
|
|
17
23
|
// <div class="nk-cover"><img src="…" alt=""></div><div class="card-title">🧭 …</div><div class="card-meta">…</div></div>
|
|
24
|
+
// <div class="card-item" role="listitem"><a class="nk-card" href="/courses/safety">…</a></div>
|
|
18
25
|
// <div class="nk-new-row">+ New page</div></div>
|
|
19
26
|
const SIZES = ['small', 'large'];
|
|
20
27
|
|
|
21
28
|
class NkGalleryView extends NkElement {
|
|
22
|
-
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'cover-key', 'no-cover', 'size', 'fit', 'title-key', 'meta-keys', 'new-row', 'new-row-label']; }
|
|
29
|
+
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'cover-key', 'no-cover', 'size', 'fit', 'title-key', 'meta-keys', 'href-key', 'new-row', 'new-row-label']; }
|
|
23
30
|
|
|
24
31
|
render() {
|
|
25
32
|
this._grid = this.createElement('div', ['nk-gallery'], { role: 'list' });
|
|
@@ -45,9 +52,13 @@ class NkGalleryView extends NkElement {
|
|
|
45
52
|
this._grid.className = ['nk-gallery', SIZES.includes(size) && size, this.getBoolAttr('fit') && 'fit'].filter(Boolean).join(' ');
|
|
46
53
|
const title = this._titleColumn(), meta = this._metaColumns();
|
|
47
54
|
const coverKey = this.getAttribute('cover-key') || 'cover', covers = !this.getBoolAttr('no-cover');
|
|
55
|
+
const hrefKey = this.getAttribute('href-key');
|
|
48
56
|
this._grid.replaceChildren();
|
|
49
57
|
for (const row of this._rows) {
|
|
50
|
-
const
|
|
58
|
+
const link = hrefKey ? row[hrefKey] : null, href = link && typeof link === 'object' ? link.href : link;
|
|
59
|
+
const card = href
|
|
60
|
+
? this.createElement('a', ['nk-card'], { href: String(href), 'data-id': row.id ?? '' })
|
|
61
|
+
: this.createElement('div', ['nk-card'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
|
|
51
62
|
if (covers) {
|
|
52
63
|
const cover = this.createElement('div', ['nk-cover']);
|
|
53
64
|
if (row[coverKey]) cover.appendChild(this.createElement('img', [], { src: row[coverKey], alt: '' }));
|
|
@@ -60,17 +71,21 @@ class NkGalleryView extends NkElement {
|
|
|
60
71
|
const v = row[c.key];
|
|
61
72
|
if (v === undefined || v === null || v === '') continue;
|
|
62
73
|
const s = document.createElement('span');
|
|
63
|
-
if (c.type === 'progress') s.textContent = `▰ ${v}
|
|
74
|
+
if (c.type === 'progress') s.textContent = `▰ ${formatPercent(c, v)}`;
|
|
64
75
|
else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
|
|
65
76
|
else s.appendChild(renderPropertyCell(c, v, row));
|
|
66
77
|
m.appendChild(s);
|
|
67
78
|
}
|
|
68
79
|
card.append(t, m);
|
|
69
|
-
|
|
80
|
+
if (href) {
|
|
81
|
+
const item = this.createElement('div', ['card-item'], { role: 'listitem' });
|
|
82
|
+
item.appendChild(card);
|
|
83
|
+
this._grid.appendChild(item);
|
|
84
|
+
} else this._grid.appendChild(card);
|
|
70
85
|
}
|
|
71
86
|
if (this.getBoolAttr('new-row')) {
|
|
72
87
|
const add = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0', 'data-add': '' });
|
|
73
|
-
add.textContent = this.getAttribute('new-row-label') || '
|
|
88
|
+
add.textContent = this.getAttribute('new-row-label') || this.str('newPage');
|
|
74
89
|
this._grid.appendChild(add);
|
|
75
90
|
}
|
|
76
91
|
}
|
|
@@ -79,10 +94,16 @@ class NkGalleryView extends NkElement {
|
|
|
79
94
|
this._onClick = (e) => {
|
|
80
95
|
if (e.target.closest('[data-add]')) { this.emit('nk-action', { action: 'new-row' }); return; }
|
|
81
96
|
const card = e.target.closest('.nk-card');
|
|
82
|
-
if (card)
|
|
97
|
+
if (!card) return;
|
|
98
|
+
// The link contract: a plain left click is a selection, any other one the browser's.
|
|
99
|
+
const link = card.localName === 'a';
|
|
100
|
+
if (link && (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)) return;
|
|
101
|
+
const row = this._rowById(card.dataset.id);
|
|
102
|
+
if (!this.emit('nk-select', { row, id: row?.id, value: row?.id }) && link) e.preventDefault();
|
|
83
103
|
};
|
|
84
104
|
this._onKey = (e) => {
|
|
85
105
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
106
|
+
if (e.target.localName === 'a') return; // a link card: Enter is the browser's click, Space scrolls
|
|
86
107
|
if (e.target.matches?.('.nk-card, [data-add]')) { e.preventDefault(); e.target.click(); }
|
|
87
108
|
};
|
|
88
109
|
this._grid.addEventListener('click', this._onClick);
|
|
@@ -96,6 +117,7 @@ class NkGalleryView extends NkElement {
|
|
|
96
117
|
|
|
97
118
|
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
98
119
|
onAttributeChanged() { this._render(); }
|
|
120
|
+
onStringsChanged() { this._render(); }
|
|
99
121
|
|
|
100
122
|
get name() { return this.getAttribute('name') || 'gallery'; }
|
|
101
123
|
get columns() { return this._columns; }
|
|
@@ -53,7 +53,7 @@ class NkListView extends NkElement {
|
|
|
53
53
|
item.append(icon, t, m);
|
|
54
54
|
this._list.appendChild(item);
|
|
55
55
|
}
|
|
56
|
-
this._newRow.textContent = this.getAttribute('new-row-label') || '
|
|
56
|
+
this._newRow.textContent = this.getAttribute('new-row-label') || this.str('newPage');
|
|
57
57
|
this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -78,6 +78,7 @@ class NkListView extends NkElement {
|
|
|
78
78
|
|
|
79
79
|
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
80
80
|
onAttributeChanged() { this._render(); }
|
|
81
|
+
onStringsChanged() { this._render(); }
|
|
81
82
|
|
|
82
83
|
get name() { return this.getAttribute('name') || 'list'; }
|
|
83
84
|
get columns() { return this._columns; }
|
|
@@ -63,7 +63,7 @@ class NkTableView extends NkElement {
|
|
|
63
63
|
}
|
|
64
64
|
this._tbody.appendChild(r);
|
|
65
65
|
}
|
|
66
|
-
this._newRow.textContent = this.getAttribute('new-row-label') || '
|
|
66
|
+
this._newRow.textContent = this.getAttribute('new-row-label') || this.str('newPage');
|
|
67
67
|
this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -113,6 +113,7 @@ class NkTableView extends NkElement {
|
|
|
113
113
|
|
|
114
114
|
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
115
115
|
onAttributeChanged() { this._render(); }
|
|
116
|
+
onStringsChanged() { this._render(); }
|
|
116
117
|
|
|
117
118
|
get name() { return this.getAttribute('name') || 'table'; }
|
|
118
119
|
get columns() { return this._columns; }
|
|
@@ -21,8 +21,9 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
21
21
|
// default from the locale, Monday where the browser cannot say), weeks,
|
|
22
22
|
// weekend (weekdays not worked, "6,0" for Saturday and Sunday), range, time,
|
|
23
23
|
// clearable, label and the texts today-label, prev-label, next-label,
|
|
24
|
-
// week-label, time-label, clear-label
|
|
25
|
-
// range has
|
|
24
|
+
// week-label, time-label, clear-label – without them English or German
|
|
25
|
+
// after the language (1.17.0). `time` belongs to single days; a range has
|
|
26
|
+
// none.
|
|
26
27
|
// days (property, or JSON attribute): { 'YYYY-MM-DD': { off, label, marks } }
|
|
27
28
|
// – `off` greys a holiday, `label` names the day (tooltip and screen
|
|
28
29
|
// reader), `marks` puts up to three dots in the nine colours on it.
|
|
@@ -151,9 +152,9 @@ class NkCalendar extends NkFormElement {
|
|
|
151
152
|
const label = this.getAttribute('label');
|
|
152
153
|
if (label) this._cal.setAttribute('aria-label', label); else this._cal.removeAttribute('aria-label');
|
|
153
154
|
this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
|
|
154
|
-
this._todayBtn.textContent = this.getAttribute('today-label') || '
|
|
155
|
-
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || '
|
|
156
|
-
this._next.setAttribute('aria-label', this.getAttribute('next-label') || '
|
|
155
|
+
this._todayBtn.textContent = this.getAttribute('today-label') || this.str('today');
|
|
156
|
+
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || this.str('previousMonth'));
|
|
157
|
+
this._next.setAttribute('aria-label', this.getAttribute('next-label') || this.str('nextMonth'));
|
|
157
158
|
|
|
158
159
|
// The one tab stop: the focused day, else the chosen one, else the 1st.
|
|
159
160
|
const cells = Array.from({ length: 42 }, (_, i) => isoOf(g.day(i)));
|
|
@@ -190,8 +191,8 @@ class NkCalendar extends NkFormElement {
|
|
|
190
191
|
|
|
191
192
|
const time = this._hasTime, clearable = this.getBoolAttr('clearable');
|
|
192
193
|
this._timeInput.value = this._time;
|
|
193
|
-
this._timeInput.setAttribute('aria-label', this.getAttribute('time-label') || '
|
|
194
|
-
this._clear.textContent = this.getAttribute('clear-label') || '
|
|
194
|
+
this._timeInput.setAttribute('aria-label', this.getAttribute('time-label') || this.str('time'));
|
|
195
|
+
this._clear.textContent = this.getAttribute('clear-label') || this.str('clear');
|
|
195
196
|
this._foot.replaceChildren(...[time && this._timeInput, clearable && this._clear].filter(Boolean));
|
|
196
197
|
if (time || clearable) this._cal.appendChild(this._foot); else this._foot.remove();
|
|
197
198
|
this._syncForm();
|
|
@@ -301,6 +302,8 @@ class NkCalendar extends NkFormElement {
|
|
|
301
302
|
document.removeEventListener('click', this._onOutside, true);
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
onStringsChanged() { if (this._grid) this._draw(); }
|
|
306
|
+
|
|
304
307
|
onAttributeChanged(name, _old, value) {
|
|
305
308
|
if (name === 'value') { this._readValue(value); this._draw(); return; }
|
|
306
309
|
if (name === 'month') { this.month = value; return; }
|
|
@@ -3,16 +3,31 @@ import { NkElement } from '../../base.js';
|
|
|
3
3
|
// <nk-copy-field value="https://monahilft.notionkit.app"></nk-copy-field>
|
|
4
4
|
// <nk-copy-field value="ntn_4f2…" secret mono></nk-copy-field>
|
|
5
5
|
// → <div class="nk-copy-field mono"><span class="cf-value">…</span>
|
|
6
|
-
// <button class="cf-btn"
|
|
6
|
+
// <button class="cf-btn" aria-label="Show"><svg>…</svg><span>Show</span></button>
|
|
7
|
+
// <button class="cf-btn" aria-label="Copy"><svg>…</svg><span>Copy</span></button></div>
|
|
7
8
|
// A value to take along – a link, an address, a key (NotionKit 1.8.0). Copy
|
|
8
9
|
// writes it to the clipboard and says "Copied" in green for a moment; where
|
|
9
10
|
// the clipboard is not allowed, the value is shown and selected instead, for
|
|
10
11
|
// ⌘C. `secret` masks it behind a Show/Hide toggle; Copy still copies the
|
|
11
12
|
// real value. `value` set as a property is not reflected, so a key never
|
|
12
13
|
// lands in the markup. Fires nk-action { action: 'copy', value, ok }. The
|
|
13
|
-
// texts
|
|
14
|
+
// texts come in English or German after the language (1.17.0, setStrings()
|
|
15
|
+
// for others); copy-label, copied-label, show-label, hide-label set them here.
|
|
16
|
+
// Each button carries an icon beside its word (1.18.0): on a phone, and with
|
|
17
|
+
// `icons` anywhere, the icon stands in for the word – two overlapping squares,
|
|
18
|
+
// an eye, a check in green for the moment after – and a masked key keeps a
|
|
19
|
+
// dozen characters more. The word stays the button's name, and while the
|
|
20
|
+
// icons show, its tooltip for <nk-tooltip>.
|
|
21
|
+
const ICONS = {
|
|
22
|
+
copy: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 9V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h4"/><rect x="9" y="9" width="12" height="12" rx="2"/></svg>',
|
|
23
|
+
copied: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
24
|
+
show: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
25
|
+
hide: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/><path d="m3 3 18 18"/></svg>',
|
|
26
|
+
};
|
|
27
|
+
const PHONE = typeof matchMedia === 'function' ? matchMedia('(max-width: 860px)') : null;
|
|
28
|
+
|
|
14
29
|
class NkCopyField extends NkElement {
|
|
15
|
-
static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
|
|
30
|
+
static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'icons', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
|
|
16
31
|
|
|
17
32
|
render() {
|
|
18
33
|
this._val ??= this.getAttribute('value') ?? '';
|
|
@@ -27,32 +42,46 @@ class NkCopyField extends NkElement {
|
|
|
27
42
|
|
|
28
43
|
_sync() {
|
|
29
44
|
if (!this._box) return;
|
|
30
|
-
for (const m of ['mono', 'wrap', 'wide']) this._box.classList.toggle(m, this.getBoolAttr(m));
|
|
45
|
+
for (const m of ['mono', 'wrap', 'wide', 'icons']) this._box.classList.toggle(m, this.getBoolAttr(m));
|
|
31
46
|
const secret = this.getBoolAttr('secret');
|
|
32
47
|
this._text.textContent = secret && !this._revealed ? '•'.repeat(Math.min(this._val.length, 24)) : this._val;
|
|
33
48
|
// Not `hidden`: .cf-btn sets no display of its own that could lose to it,
|
|
34
49
|
// but a button that is not there cannot be tabbed to either.
|
|
35
50
|
if (secret && !this._show.isConnected) this._copy.before(this._show);
|
|
36
51
|
if (!secret) this._show.remove();
|
|
37
|
-
|
|
52
|
+
const tip = this.getBoolAttr('icons') || !!PHONE?.matches;
|
|
53
|
+
this._button(this._show, this._revealed ? 'hide' : 'show', this._revealed ? (this.getAttribute('hide-label') || this.str('hide')) : (this.getAttribute('show-label') || this.str('show')), tip);
|
|
38
54
|
this._show.setAttribute('aria-pressed', String(!!this._revealed));
|
|
39
|
-
this._copy.
|
|
55
|
+
this._button(this._copy, this._copied ? 'copied' : 'copy', this._copied ? (this.getAttribute('copied-label') || this.str('copied')) : (this.getAttribute('copy-label') || this.str('copy')), tip);
|
|
40
56
|
this._copy.classList.toggle('copied', !!this._copied);
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
/** The icon and the word of a button; the word is its name, and its tooltip while the icon stands in for it. */
|
|
60
|
+
_button(btn, icon, word, tip) {
|
|
61
|
+
if (btn.dataset.icon !== icon) { btn.innerHTML = `${ICONS[icon]}<span></span>`; btn.dataset.icon = icon; }
|
|
62
|
+
btn.lastChild.textContent = word;
|
|
63
|
+
btn.setAttribute('aria-label', word);
|
|
64
|
+
if (tip) btn.setAttribute('data-tooltip', word); else btn.removeAttribute('data-tooltip');
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
setupEvents() {
|
|
44
68
|
this._onCopy = () => this.copy();
|
|
45
69
|
this._onShow = () => { this._revealed = !this._revealed; this._sync(); };
|
|
70
|
+
this._onPhone = () => this._sync();
|
|
46
71
|
this._copy.addEventListener('click', this._onCopy);
|
|
47
72
|
this._show.addEventListener('click', this._onShow);
|
|
73
|
+
PHONE?.addEventListener('change', this._onPhone);
|
|
48
74
|
}
|
|
49
75
|
|
|
50
76
|
teardownEvents() {
|
|
51
77
|
this._copy?.removeEventListener('click', this._onCopy);
|
|
52
78
|
this._show?.removeEventListener('click', this._onShow);
|
|
79
|
+
PHONE?.removeEventListener('change', this._onPhone);
|
|
53
80
|
clearTimeout(this._timer);
|
|
54
81
|
}
|
|
55
82
|
|
|
83
|
+
onStringsChanged() { this._sync(); }
|
|
84
|
+
|
|
56
85
|
onAttributeChanged(name, _old, value) {
|
|
57
86
|
if (name === 'value') this._val = value ?? '';
|
|
58
87
|
this._sync();
|
|
@@ -80,6 +109,8 @@ class NkCopyField extends NkElement {
|
|
|
80
109
|
set value(v) { this._val = v == null ? '' : String(v); this._sync(); }
|
|
81
110
|
get secret() { return this.getBoolAttr('secret'); }
|
|
82
111
|
set secret(v) { this.setBoolAttr('secret', v); }
|
|
112
|
+
get icons() { return this.getBoolAttr('icons'); }
|
|
113
|
+
set icons(v) { this.setBoolAttr('icons', v); }
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
customElements.define('nk-copy-field', NkCopyField);
|
|
@@ -20,7 +20,9 @@ import { NkElement } from '../../base.js';
|
|
|
20
20
|
// Events: nk-change { dataUrl, width, height, size } after a pick, and on
|
|
21
21
|
// remove with an empty dataUrl; nk-error { message, name } when the
|
|
22
22
|
// file cannot be decoded (HEIC outside Safari, a broken file).
|
|
23
|
-
// Texts:
|
|
23
|
+
// Texts: English or German after the language (1.17.0, setStrings() for
|
|
24
|
+
// others); choose-label, change-label, remove-label set them here.
|
|
25
|
+
// label names the group.
|
|
24
26
|
|
|
25
27
|
function dataUrlBytes(url) {
|
|
26
28
|
const b64 = url.slice(url.indexOf(',') + 1);
|
|
@@ -62,8 +64,8 @@ class NkImagePicker extends NkElement {
|
|
|
62
64
|
}
|
|
63
65
|
const label = this.getAttribute('label');
|
|
64
66
|
if (label) this._row.setAttribute('aria-label', label); else this._row.removeAttribute('aria-label');
|
|
65
|
-
this._choose.textContent = this._src ? (this.getAttribute('change-label') || '
|
|
66
|
-
this._remove.textContent = this.getAttribute('remove-label') || '
|
|
67
|
+
this._choose.textContent = this._src ? (this.getAttribute('change-label') || this.str('changeImage')) : (this.getAttribute('choose-label') || this.str('uploadImage'));
|
|
68
|
+
this._remove.textContent = this.getAttribute('remove-label') || this.str('remove');
|
|
67
69
|
this._remove.hidden = !this._src;
|
|
68
70
|
this._input.accept = this.getAttribute('accept') || 'image/*';
|
|
69
71
|
}
|
|
@@ -111,6 +113,8 @@ class NkImagePicker extends NkElement {
|
|
|
111
113
|
this._remove?.removeEventListener('click', this._onRemove);
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
onStringsChanged() { this._sync(); }
|
|
117
|
+
|
|
114
118
|
onAttributeChanged(name, _old, value) {
|
|
115
119
|
if (name === 'src') this._src = value || '';
|
|
116
120
|
this._sync();
|
|
@@ -18,7 +18,7 @@ class NkCmdk extends NkElement {
|
|
|
18
18
|
render() {
|
|
19
19
|
this._backdrop = this.createElement('div', ['nk-cmdk-backdrop']);
|
|
20
20
|
this._backdrop.inert = true;
|
|
21
|
-
this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true'
|
|
21
|
+
this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true' });
|
|
22
22
|
const row = this.createElement('div', ['nk-cmdk-input-row']);
|
|
23
23
|
const glass = document.createElement('span');
|
|
24
24
|
glass.style.fontSize = '15px';
|
|
@@ -43,7 +43,11 @@ class NkCmdk extends NkElement {
|
|
|
43
43
|
this._syncOpen();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
_syncAttrs() {
|
|
46
|
+
_syncAttrs() {
|
|
47
|
+
this._input.placeholder = this.getAttribute('placeholder') || this.str('commandPlaceholder');
|
|
48
|
+
this._box.setAttribute('aria-label', this.str('commandPalette'));
|
|
49
|
+
}
|
|
50
|
+
onStringsChanged() { this._syncAttrs(); }
|
|
47
51
|
|
|
48
52
|
/** Filtered, flat list in group order, best matches first within a group. */
|
|
49
53
|
results() {
|
|
@@ -10,7 +10,7 @@ class NkEmojiPicker extends NkElement {
|
|
|
10
10
|
|
|
11
11
|
render() {
|
|
12
12
|
this._box = this.createElement('div', ['nk-pop']);
|
|
13
|
-
this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off'
|
|
13
|
+
this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off' });
|
|
14
14
|
this._grid = this.createElement('div', ['nk-emoji-grid'], { role: 'listbox' });
|
|
15
15
|
this._cats = this.createElement('div', ['nk-emoji-cats'], { role: 'tablist' });
|
|
16
16
|
this._box.append(this._search, this._grid, this._cats);
|
|
@@ -22,7 +22,11 @@ class NkEmojiPicker extends NkElement {
|
|
|
22
22
|
this._syncAttrs();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
_syncAttrs() {
|
|
25
|
+
_syncAttrs() {
|
|
26
|
+
this._search.placeholder = this.getAttribute('placeholder') || this.str('emojiPlaceholder');
|
|
27
|
+
this._search.setAttribute('aria-label', this.str('searchEmoji'));
|
|
28
|
+
}
|
|
29
|
+
onStringsChanged() { this._syncAttrs(); }
|
|
26
30
|
|
|
27
31
|
_renderCats() {
|
|
28
32
|
this._cats.replaceChildren();
|