@jungherz-de/notionkit-elements 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +64 -6
  3. package/SKILL.md +59 -53
  4. package/dist/components/base.js +102 -3
  5. package/dist/components/nk-ai-input-row.js +6 -3
  6. package/dist/components/nk-board-view.js +2 -2
  7. package/dist/components/nk-breadcrumb.js +2 -1
  8. package/dist/components/nk-calendar-view.js +8 -5
  9. package/dist/components/nk-calendar.js +10 -7
  10. package/dist/components/nk-cmdk.js +6 -2
  11. package/dist/components/nk-comments.js +5 -3
  12. package/dist/components/nk-copy-field.js +6 -3
  13. package/dist/components/nk-emoji-picker.js +6 -2
  14. package/dist/components/nk-filter-bar.js +10 -8
  15. package/dist/components/nk-gallery-view.js +29 -7
  16. package/dist/components/nk-image-picker.js +7 -3
  17. package/dist/components/nk-list-view.js +3 -2
  18. package/dist/components/nk-modal.js +3 -1
  19. package/dist/components/nk-page-title.js +2 -1
  20. package/dist/components/nk-page.js +3 -1
  21. package/dist/components/nk-peek.js +6 -3
  22. package/dist/components/nk-sidebar.js +5 -2
  23. package/dist/components/nk-steps.js +20 -4
  24. package/dist/components/nk-synced.js +2 -1
  25. package/dist/components/nk-table-view.js +3 -2
  26. package/dist/components/nk-theme-toggle.js +5 -2
  27. package/dist/components/nk-tree-item.js +3 -1
  28. package/dist/components/shared/{property-cell-BSVcETss.js → property-cell-DcsfnUNF.js} +15 -5
  29. package/dist/notionkit-elements.esm.js +265 -70
  30. package/dist/notionkit-elements.esm.min.js +1 -1
  31. package/dist/notionkit-elements.esm.min.js.map +1 -1
  32. package/dist/notionkit-elements.js +267 -69
  33. package/dist/notionkit-elements.min.js +1 -1
  34. package/package.json +3 -3
  35. package/src/base.js +32 -2
  36. package/src/components/data/nk-ai-input-row.js +6 -3
  37. package/src/components/data/nk-board-view.js +2 -2
  38. package/src/components/data/nk-calendar-view.js +7 -4
  39. package/src/components/data/nk-comments.js +5 -3
  40. package/src/components/data/nk-filter-bar.js +10 -8
  41. package/src/components/data/nk-gallery-view.js +29 -7
  42. package/src/components/data/nk-list-view.js +2 -1
  43. package/src/components/data/nk-table-view.js +2 -1
  44. package/src/components/forms/nk-calendar.js +10 -7
  45. package/src/components/forms/nk-copy-field.js +6 -3
  46. package/src/components/forms/nk-image-picker.js +7 -3
  47. package/src/components/overlays/nk-cmdk.js +6 -2
  48. package/src/components/overlays/nk-emoji-picker.js +6 -2
  49. package/src/components/overlays/nk-modal.js +3 -1
  50. package/src/components/overlays/nk-peek.js +6 -3
  51. package/src/components/page/nk-page-title.js +2 -1
  52. package/src/components/page/nk-page.js +3 -1
  53. package/src/components/page/nk-steps.js +20 -4
  54. package/src/components/page/nk-synced.js +2 -1
  55. package/src/components/shell/nk-breadcrumb.js +2 -1
  56. package/src/components/shell/nk-sidebar.js +5 -2
  57. package/src/components/shell/nk-theme-toggle.js +5 -2
  58. package/src/components/shell/nk-tree-item.js +3 -1
  59. package/src/index.js +5 -1
  60. package/src/util/property-cell.js +14 -4
  61. 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.16.0",
3
+ "version": "1.17.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.16.0"
66
+ "@jungherz-de/notionkit": ">=1.17.0"
67
67
  },
68
68
  "devDependencies": {
69
- "@jungherz-de/notionkit": "^1.16.0",
69
+ "@jungherz-de/notionkit": "^1.17.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(syncAllThemes).observe(document.documentElement, {
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';
@@ -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', 'aria-label': 'Message' });
14
- this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button', 'aria-label': 'Send' });
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') || 'Ask something …';
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') || 'Today';
70
- this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
71
- this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
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', 'aria-label': 'Comment' });
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') || 'Comment …';
25
- this._btn.textContent = this.getAttribute('send-label') || 'Send';
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 are
16
- // attributes, for other languages: filter-label, sort-label, add-label,
17
- // remove-label (the × reads "<remove-label>: <pill>") and placeholder.
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') || 'Filter';
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') || 'Sort';
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') || '+ Filter';
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') || 'Search …';
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') || 'Remove filter';
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 card = this.createElement('div', ['nk-card'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
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
- this._grid.appendChild(card);
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') || '+ New page';
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) { const row = this._rowById(card.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
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') || '+ New page';
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') || '+ New page';
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. `time` belongs to single days; a
25
- // range has none.
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') || 'Today';
155
- this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
156
- this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
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') || 'Time');
194
- this._clear.textContent = this.getAttribute('clear-label') || 'Clear';
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; }
@@ -10,7 +10,8 @@ import { NkElement } from '../../base.js';
10
10
  // ⌘C. `secret` masks it behind a Show/Hide toggle; Copy still copies the
11
11
  // real value. `value` set as a property is not reflected, so a key never
12
12
  // lands in the markup. Fires nk-action { action: 'copy', value, ok }. The
13
- // texts are attributes: copy-label, copied-label, show-label, hide-label.
13
+ // texts come in English or German after the language (1.17.0, setStrings()
14
+ // for others); copy-label, copied-label, show-label, hide-label set them here.
14
15
  class NkCopyField extends NkElement {
15
16
  static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
16
17
 
@@ -34,9 +35,9 @@ class NkCopyField extends NkElement {
34
35
  // but a button that is not there cannot be tabbed to either.
35
36
  if (secret && !this._show.isConnected) this._copy.before(this._show);
36
37
  if (!secret) this._show.remove();
37
- this._show.textContent = this._revealed ? (this.getAttribute('hide-label') || 'Hide') : (this.getAttribute('show-label') || 'Show');
38
+ this._show.textContent = this._revealed ? (this.getAttribute('hide-label') || this.str('hide')) : (this.getAttribute('show-label') || this.str('show'));
38
39
  this._show.setAttribute('aria-pressed', String(!!this._revealed));
39
- this._copy.textContent = this._copied ? (this.getAttribute('copied-label') || 'Copied') : (this.getAttribute('copy-label') || 'Copy');
40
+ this._copy.textContent = this._copied ? (this.getAttribute('copied-label') || this.str('copied')) : (this.getAttribute('copy-label') || this.str('copy'));
40
41
  this._copy.classList.toggle('copied', !!this._copied);
41
42
  }
42
43
 
@@ -53,6 +54,8 @@ class NkCopyField extends NkElement {
53
54
  clearTimeout(this._timer);
54
55
  }
55
56
 
57
+ onStringsChanged() { this._sync(); }
58
+
56
59
  onAttributeChanged(name, _old, value) {
57
60
  if (name === 'value') this._val = value ?? '';
58
61
  this._sync();
@@ -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: choose-label, change-label, remove-label; label names the group.
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') || 'Change image') : (this.getAttribute('choose-label') || 'Upload image');
66
- this._remove.textContent = this.getAttribute('remove-label') || 'Remove';
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', 'aria-label': 'Command palette' });
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() { this._input.placeholder = this.getAttribute('placeholder') || 'Search or type a command …'; }
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', 'aria-label': 'Search emoji' });
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() { this._search.placeholder = this.getAttribute('placeholder') || 'Search…'; }
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();
@@ -22,7 +22,7 @@ class NkModal extends NkElement {
22
22
  this._backdrop = this.createElement('div', ['nk-modal-backdrop']);
23
23
  this._backdrop.inert = true;
24
24
  this._box = this.createElement('div', ['nk-modal'], { role: 'dialog', 'aria-modal': 'true', tabindex: '-1' });
25
- this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': 'Settings' });
25
+ this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': this.str('settings') });
26
26
  this._nav.appendChild(this.createElement('slot', [], { name: 'user' }));
27
27
  this._rows = document.createElement('div');
28
28
  this._nav.appendChild(this._rows);
@@ -140,6 +140,8 @@ class NkModal extends NkElement {
140
140
  if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
141
141
  }
142
142
 
143
+ onStringsChanged() { this._nav.setAttribute('aria-label', this.str('settings')); }
144
+
143
145
  onAttributeChanged(name) {
144
146
  if (name === 'open') {
145
147
  this._syncOpen();
@@ -26,7 +26,8 @@ import { lockScroll, unlockScroll, inertOutside, deepActiveElement } from '../..
26
26
  // Notion: from `min` (380) to `max` (all but 320px of the window). The width
27
27
  // is the token --nk-peek-width, set on :root; `width` (px) sets it too, and
28
28
  // nk-resize { width } fires when a drag or a key ends – the moment to keep
29
- // it. resize-label names the edge (default "Resize").
29
+ // it. resize-label names the edge (default "Resize", in German "Breite
30
+ // ändern").
30
31
  // `inset`: while it is open on the desktop, the page's <nk-app> gets
31
32
  // `peek-inset` – its main column makes room instead of lying under the peek.
32
33
  const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 17 5-5-5-5M13 17l5-5-5-5"/></svg>';
@@ -73,7 +74,7 @@ class NkPeek extends NkElement {
73
74
 
74
75
  _syncResize() {
75
76
  this._handle.hidden = !this.getBoolAttr('resizable');
76
- this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || 'Resize');
77
+ this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || this.str('resize'));
77
78
  this._handle.setAttribute('aria-valuemin', this._min);
78
79
  this._handle.setAttribute('aria-valuenow', Math.round(this.width));
79
80
  }
@@ -90,7 +91,7 @@ class NkPeek extends NkElement {
90
91
  _syncLabels() {
91
92
  const label = this.getAttribute('label');
92
93
  if (label) this._box.setAttribute('aria-label', label); else this._box.removeAttribute('aria-label');
93
- this._close.setAttribute('aria-label', this.getAttribute('close-label') || 'Close');
94
+ this._close.setAttribute('aria-label', this.getAttribute('close-label') || this.str('close'));
94
95
  }
95
96
 
96
97
  _syncOpen() {
@@ -175,6 +176,8 @@ class NkPeek extends NkElement {
175
176
  this._wasOpen = false;
176
177
  }
177
178
 
179
+ onStringsChanged() { this._syncResize(); this._syncLabels(); }
180
+
178
181
  onAttributeChanged(name, _old, value) {
179
182
  if (name === 'width') { this._setWidth(Number(value)); return; }
180
183
  if (name === 'inset') { this._syncInset(); return; }
@@ -23,7 +23,7 @@ class NkPageTitle extends NkElement {
23
23
  this._h1.setAttribute('contenteditable', 'plaintext-only');
24
24
  this._h1.setAttribute('spellcheck', 'false');
25
25
  this._h1.setAttribute('role', 'textbox');
26
- this._h1.setAttribute('aria-label', 'Title');
26
+ this._h1.setAttribute('aria-label', this.str('title'));
27
27
  const text = this.getAttribute('value') ?? this.textContent.trim();
28
28
  if (this._h1.textContent !== text && document.activeElement !== this) this._h1.textContent = text;
29
29
  this._h1.setAttribute('data-placeholder', this.getAttribute('placeholder') || '');
@@ -58,6 +58,7 @@ class NkPageTitle extends NkElement {
58
58
  }
59
59
 
60
60
  onAttributeChanged() { this._sync(); }
61
+ onStringsChanged() { if (this._h1.hasAttribute('role')) this._h1.setAttribute('aria-label', this.str('title')); }
61
62
 
62
63
  get value() { return this.getBoolAttr('editable') ? (this._h1?.textContent.trim() ?? '') : (this.getAttribute('value') ?? this.textContent.trim()); }
63
64
  set value(v) { this.setAttribute('value', v); }
@@ -19,7 +19,7 @@ class NkPage extends NkElement {
19
19
 
20
20
  render() {
21
21
  this._page = this.createElement('div', ['nk-page']);
22
- this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: 'Change icon' });
22
+ this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: this.str('changeIcon') });
23
23
  this._iconSlot = this.createElement('slot', [], { name: 'icon' });
24
24
  this._iconSlot.appendChild(this._icon);
25
25
  this._page.appendChild(this._iconSlot);
@@ -75,6 +75,8 @@ class NkPage extends NkElement {
75
75
  this._iconSlot?.removeEventListener('slotchange', this._onCover);
76
76
  }
77
77
 
78
+ onStringsChanged() { this._icon.title = this.str('changeIcon'); }
79
+
78
80
  onAttributeChanged(name) {
79
81
  if (name === 'narrow') this._build();
80
82
  else this._sync();