@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
@@ -11,8 +11,8 @@ class NkAiInputRow extends NkElement {
11
11
  this._row = this.createElement('div', ['nk-ai-input-row']);
12
12
  this._icon = document.createElement('span');
13
13
  this._icon.style.fontSize = '14px';
14
- this._input = this.createElement('input', [], { type: 'text', 'aria-label': 'Message' });
15
- this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button', 'aria-label': 'Send' });
14
+ this._input = this.createElement('input', [], { type: 'text' });
15
+ this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button' });
16
16
  this._btn.textContent = '↑';
17
17
  this._row.append(this._icon, this._input, this._btn);
18
18
  this._wrapper.appendChild(this._row);
@@ -20,7 +20,9 @@ class NkAiInputRow extends NkElement {
20
20
  }
21
21
 
22
22
  _sync() {
23
- this._input.placeholder = this.getAttribute('placeholder') || 'Ask something …';
23
+ this._input.placeholder = this.getAttribute('placeholder') || this.str('askPlaceholder');
24
+ this._input.setAttribute('aria-label', this.str('message'));
25
+ this._btn.setAttribute('aria-label', this.str('send'));
24
26
  if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
25
27
  this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
26
28
  const icon = this.getAttribute('icon') ?? '✨';
@@ -48,6 +50,7 @@ class NkAiInputRow extends NkElement {
48
50
  }
49
51
 
50
52
  onAttributeChanged() { this._sync(); }
53
+ onStringsChanged() { this._sync(); }
51
54
  focus(o) { this._input?.focus(o); }
52
55
  get value() { return this._input?.value ?? ''; }
53
56
  set value(v) { if (this._input) this._input.value = v ?? ''; }
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { t as tagFor, a as textOf, f as formatDate, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
2
+ import { t as tagFor, a as textOf, f as formatPercent, b as formatDate, r as renderPropertyCell } from './shared/property-cell-DcsfnUNF.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
  import './shared/avatar-CzPZN3uP.js';
5
5
  import './shared/dates-a32DcW1l.js';
@@ -58,7 +58,7 @@ class NkBoardView extends NkElement {
58
58
  const v = row[c.key];
59
59
  if (v === undefined || v === null || v === '') continue;
60
60
  const s = document.createElement('span');
61
- if (c.type === 'progress') s.textContent = `▰ ${v}%`;
61
+ if (c.type === 'progress') s.textContent = `▰ ${formatPercent(c, v)}`;
62
62
  else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
63
63
  else s.appendChild(renderPropertyCell(c, v, row));
64
64
  meta.appendChild(s);
@@ -17,7 +17,7 @@ class NkBreadcrumb extends NkElement {
17
17
  static get observesLightDom() { return true; }
18
18
 
19
19
  render() {
20
- this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': 'Breadcrumb' });
20
+ this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': this.str('breadcrumb') });
21
21
  this._wrapper.appendChild(this._nav);
22
22
  this.projectLightDom();
23
23
  }
@@ -69,6 +69,7 @@ class NkBreadcrumb extends NkElement {
69
69
  teardownEvents() { this._nav?.removeEventListener('click', this._onClick); }
70
70
 
71
71
  onAttributeChanged() { this.projectLightDom(); }
72
+ onStringsChanged() { this._nav.setAttribute('aria-label', this.str('breadcrumb')); }
72
73
 
73
74
  get separator() { return this.getAttribute('separator') ?? '/'; }
74
75
  set separator(v) { this.setAttribute('separator', v); }
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { a as textOf } from './shared/property-cell-BSVcETss.js';
2
+ import { a as textOf } from './shared/property-cell-DcsfnUNF.js';
3
3
  import { C as CHEVRONS, i as isoOf, r as resolveLocale, f as firstWeekday, m as monthGrid, p as parseDay, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth } from './shared/dates-a32DcW1l.js';
4
4
  import '@jungherz-de/notionkit/notionkit-styles.js';
5
5
  import './shared/avatar-CzPZN3uP.js';
@@ -12,7 +12,8 @@ import './shared/avatar-CzPZN3uP.js';
12
12
  // default today's), weeks (the ISO week in front of each row), week-start
13
13
  // (0 = Sunday … 6 = Saturday; default from the locale), weekend (weekdays
14
14
  // washed like the days of other months, "6,0"), today, locale and the texts
15
- // today-label, prev-label, next-label, week-label.
15
+ // today-label, prev-label, next-label, week-label – without them English
16
+ // or German after the language (1.17.0).
16
17
  // Standalone: view.columns = […]; view.rows = […]. Inside <nk-database> the
17
18
  // database pushes the data. A card fires nk-select { row, id, value } like a
18
19
  // row of the table, Today and ‹ › nk-month { month }.
@@ -68,9 +69,9 @@ class NkCalendarView extends NkElement {
68
69
  }
69
70
  this._box.classList.toggle('weeks', weeks);
70
71
  this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
71
- this._todayBtn.textContent = this.getAttribute('today-label') || 'Today';
72
- this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
73
- this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
72
+ this._todayBtn.textContent = this.getAttribute('today-label') || this.str('today');
73
+ this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || this.str('previousMonth'));
74
+ this._next.setAttribute('aria-label', this.getAttribute('next-label') || this.str('nextMonth'));
74
75
 
75
76
  const cell = (cls, text) => { const el = this.createElement('div', [cls]); el.textContent = text; return el; };
76
77
  const nodes = [];
@@ -116,6 +117,8 @@ class NkCalendarView extends NkElement {
116
117
 
117
118
  _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
118
119
 
120
+ onStringsChanged() { this._render(); }
121
+
119
122
  onAttributeChanged(name) {
120
123
  if (name === 'month') this._month = this._attrMonth() ?? this._month;
121
124
  this._render();
@@ -22,8 +22,9 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
22
22
  // default from the locale, Monday where the browser cannot say), weeks,
23
23
  // weekend (weekdays not worked, "6,0" for Saturday and Sunday), range, time,
24
24
  // clearable, label and the texts today-label, prev-label, next-label,
25
- // week-label, time-label, clear-label. `time` belongs to single days; a
26
- // range has none.
25
+ // week-label, time-label, clear-label – without them English or German
26
+ // after the language (1.17.0). `time` belongs to single days; a range has
27
+ // none.
27
28
  // days (property, or JSON attribute): { 'YYYY-MM-DD': { off, label, marks } }
28
29
  // – `off` greys a holiday, `label` names the day (tooltip and screen
29
30
  // reader), `marks` puts up to three dots in the nine colours on it.
@@ -152,9 +153,9 @@ class NkCalendar extends NkFormElement {
152
153
  const label = this.getAttribute('label');
153
154
  if (label) this._cal.setAttribute('aria-label', label); else this._cal.removeAttribute('aria-label');
154
155
  this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
155
- this._todayBtn.textContent = this.getAttribute('today-label') || 'Today';
156
- this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
157
- this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
156
+ this._todayBtn.textContent = this.getAttribute('today-label') || this.str('today');
157
+ this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || this.str('previousMonth'));
158
+ this._next.setAttribute('aria-label', this.getAttribute('next-label') || this.str('nextMonth'));
158
159
 
159
160
  // The one tab stop: the focused day, else the chosen one, else the 1st.
160
161
  const cells = Array.from({ length: 42 }, (_, i) => isoOf(g.day(i)));
@@ -191,8 +192,8 @@ class NkCalendar extends NkFormElement {
191
192
 
192
193
  const time = this._hasTime, clearable = this.getBoolAttr('clearable');
193
194
  this._timeInput.value = this._time;
194
- this._timeInput.setAttribute('aria-label', this.getAttribute('time-label') || 'Time');
195
- this._clear.textContent = this.getAttribute('clear-label') || 'Clear';
195
+ this._timeInput.setAttribute('aria-label', this.getAttribute('time-label') || this.str('time'));
196
+ this._clear.textContent = this.getAttribute('clear-label') || this.str('clear');
196
197
  this._foot.replaceChildren(...[time && this._timeInput, clearable && this._clear].filter(Boolean));
197
198
  if (time || clearable) this._cal.appendChild(this._foot); else this._foot.remove();
198
199
  this._syncForm();
@@ -302,6 +303,8 @@ class NkCalendar extends NkFormElement {
302
303
  document.removeEventListener('click', this._onOutside, true);
303
304
  }
304
305
 
306
+ onStringsChanged() { if (this._grid) this._draw(); }
307
+
305
308
  onAttributeChanged(name, _old, value) {
306
309
  if (name === 'value') { this._readValue(value); this._draw(); return; }
307
310
  if (name === 'month') { this.month = value; return; }
@@ -38,7 +38,7 @@ class NkCmdk extends NkElement {
38
38
  render() {
39
39
  this._backdrop = this.createElement('div', ['nk-cmdk-backdrop']);
40
40
  this._backdrop.inert = true;
41
- this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette' });
41
+ this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true' });
42
42
  const row = this.createElement('div', ['nk-cmdk-input-row']);
43
43
  const glass = document.createElement('span');
44
44
  glass.style.fontSize = '15px';
@@ -63,7 +63,11 @@ class NkCmdk extends NkElement {
63
63
  this._syncOpen();
64
64
  }
65
65
 
66
- _syncAttrs() { this._input.placeholder = this.getAttribute('placeholder') || 'Search or type a command …'; }
66
+ _syncAttrs() {
67
+ this._input.placeholder = this.getAttribute('placeholder') || this.str('commandPlaceholder');
68
+ this._box.setAttribute('aria-label', this.str('commandPalette'));
69
+ }
70
+ onStringsChanged() { this._syncAttrs(); }
67
71
 
68
72
  /** Filtered, flat list in group order, best matches first within a group. */
69
73
  results() {
@@ -13,7 +13,7 @@ class NkComments extends NkElement {
13
13
  const box = this.createElement('div', ['nk-comments']);
14
14
  box.appendChild(document.createElement('slot'));
15
15
  this._row = this.createElement('div', ['nk-comment-input']);
16
- this._input = this.createElement('input', ['nk-input'], { type: 'text', 'aria-label': 'Comment' });
16
+ this._input = this.createElement('input', ['nk-input'], { type: 'text' });
17
17
  this._btn = this.createElement('button', ['nk-btn', 'primary', 'small'], { type: 'button' });
18
18
  this._row.append(this._input, this._btn);
19
19
  box.appendChild(this._row);
@@ -22,8 +22,9 @@ class NkComments extends NkElement {
22
22
  }
23
23
 
24
24
  _sync() {
25
- this._input.placeholder = this.getAttribute('placeholder') || 'Comment …';
26
- this._btn.textContent = this.getAttribute('send-label') || 'Send';
25
+ this._input.placeholder = this.getAttribute('placeholder') || this.str('commentPlaceholder');
26
+ this._input.setAttribute('aria-label', this.str('comment'));
27
+ this._btn.textContent = this.getAttribute('send-label') || this.str('send');
27
28
  this._row.style.display = this.getBoolAttr('no-input') ? 'none' : '';
28
29
  this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
29
30
  }
@@ -48,6 +49,7 @@ class NkComments extends NkElement {
48
49
  }
49
50
 
50
51
  onAttributeChanged() { this._sync(); }
52
+ onStringsChanged() { this._sync(); }
51
53
  focus(o) { this._input?.focus(o); }
52
54
  get value() { return this._input?.value ?? ''; }
53
55
  set value(v) { if (this._input) this._input.value = v ?? ''; }
@@ -11,7 +11,8 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
11
11
  // ⌘C. `secret` masks it behind a Show/Hide toggle; Copy still copies the
12
12
  // real value. `value` set as a property is not reflected, so a key never
13
13
  // lands in the markup. Fires nk-action { action: 'copy', value, ok }. The
14
- // texts are attributes: copy-label, copied-label, show-label, hide-label.
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.
15
16
  class NkCopyField extends NkElement {
16
17
  static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
17
18
 
@@ -35,9 +36,9 @@ class NkCopyField extends NkElement {
35
36
  // but a button that is not there cannot be tabbed to either.
36
37
  if (secret && !this._show.isConnected) this._copy.before(this._show);
37
38
  if (!secret) this._show.remove();
38
- this._show.textContent = this._revealed ? (this.getAttribute('hide-label') || 'Hide') : (this.getAttribute('show-label') || 'Show');
39
+ this._show.textContent = this._revealed ? (this.getAttribute('hide-label') || this.str('hide')) : (this.getAttribute('show-label') || this.str('show'));
39
40
  this._show.setAttribute('aria-pressed', String(!!this._revealed));
40
- this._copy.textContent = this._copied ? (this.getAttribute('copied-label') || 'Copied') : (this.getAttribute('copy-label') || 'Copy');
41
+ this._copy.textContent = this._copied ? (this.getAttribute('copied-label') || this.str('copied')) : (this.getAttribute('copy-label') || this.str('copy'));
41
42
  this._copy.classList.toggle('copied', !!this._copied);
42
43
  }
43
44
 
@@ -54,6 +55,8 @@ class NkCopyField extends NkElement {
54
55
  clearTimeout(this._timer);
55
56
  }
56
57
 
58
+ onStringsChanged() { this._sync(); }
59
+
57
60
  onAttributeChanged(name, _old, value) {
58
61
  if (name === 'value') this._val = value ?? '';
59
62
  this._sync();
@@ -28,7 +28,7 @@ class NkEmojiPicker extends NkElement {
28
28
 
29
29
  render() {
30
30
  this._box = this.createElement('div', ['nk-pop']);
31
- this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off', 'aria-label': 'Search emoji' });
31
+ this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off' });
32
32
  this._grid = this.createElement('div', ['nk-emoji-grid'], { role: 'listbox' });
33
33
  this._cats = this.createElement('div', ['nk-emoji-cats'], { role: 'tablist' });
34
34
  this._box.append(this._search, this._grid, this._cats);
@@ -40,7 +40,11 @@ class NkEmojiPicker extends NkElement {
40
40
  this._syncAttrs();
41
41
  }
42
42
 
43
- _syncAttrs() { this._search.placeholder = this.getAttribute('placeholder') || 'Search…'; }
43
+ _syncAttrs() {
44
+ this._search.placeholder = this.getAttribute('placeholder') || this.str('emojiPlaceholder');
45
+ this._search.setAttribute('aria-label', this.str('searchEmoji'));
46
+ }
47
+ onStringsChanged() { this._syncAttrs(); }
44
48
 
45
49
  _renderCats() {
46
50
  this._cats.replaceChildren();
@@ -13,9 +13,10 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
13
13
  // nk-action { action: 'add' } – each with the clicked button as `anchor`, to
14
14
  // open a menu under it. × removes the filter; every change to filters or
15
15
  // search fires nk-change { filters, search }. In slot="filters" of
16
- // <nk-database> the row sits right under the view tabs. The texts are
17
- // attributes, for other languages: filter-label, sort-label, add-label,
18
- // remove-label (the × reads "<remove-label>: <pill>") and placeholder.
16
+ // <nk-database> the row sits right under the view tabs. The texts come in
17
+ // English or German after the language (1.17.0, setStrings() for others);
18
+ // filter-label, sort-label, add-label, remove-label (the × reads
19
+ // "<remove-label>: <pill>") and placeholder set them here.
19
20
  class NkFilterBar extends NkElement {
20
21
  static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value', 'add', 'add-label', 'filter-label', 'sort-label', 'remove-label']; }
21
22
 
@@ -33,18 +34,18 @@ class NkFilterBar extends NkElement {
33
34
  }
34
35
 
35
36
  _sync() {
36
- this._filterBtn.textContent = this.getAttribute('filter-label') || 'Filter';
37
+ this._filterBtn.textContent = this.getAttribute('filter-label') || this.str('filter');
37
38
  this._filterBtn.hidden = this.getBoolAttr('no-filter');
38
39
  this._filterBtn.classList.toggle('active', this._filters.length > 0);
39
- this._sortBtn.textContent = this.getAttribute('sort-label') || 'Sort';
40
+ this._sortBtn.textContent = this.getAttribute('sort-label') || this.str('sort');
40
41
  this._sortBtn.hidden = this.getBoolAttr('no-sort');
41
- this._addBtn.textContent = this.getAttribute('add-label') || '+ Filter';
42
+ this._addBtn.textContent = this.getAttribute('add-label') || this.str('addFilter');
42
43
  this._addBtn.hidden = !this.getBoolAttr('add');
43
44
  this._input.hidden = !this.getBoolAttr('search');
44
- this._input.placeholder = this.getAttribute('placeholder') || 'Search …';
45
+ this._input.placeholder = this.getAttribute('placeholder') || this.str('search');
45
46
  if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
46
47
  for (const old of this._row.querySelectorAll('[data-pill]')) old.remove();
47
- const removeLabel = this.getAttribute('remove-label') || 'Remove filter';
48
+ const removeLabel = this.getAttribute('remove-label') || this.str('removeFilter');
48
49
  this._pillStart.after(...this._filters.map((f, i) => {
49
50
  const pill = this.createElement('span', ['nk-filter-pill', 'active'], { 'data-pill': i });
50
51
  const label = this.createElement('button', [], { type: 'button', 'data-edit': i });
@@ -78,6 +79,7 @@ class NkFilterBar extends NkElement {
78
79
 
79
80
  _emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
80
81
  onAttributeChanged() { this._sync(); }
82
+ onStringsChanged() { this._sync(); }
81
83
 
82
84
  // A copy each way: the caller's array is never changed – removing a pill
83
85
  // only reports the new list in nk-change.
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { a as textOf, f as formatDate, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
2
+ import { a as textOf, f as formatPercent, b as formatDate, r as renderPropertyCell } from './shared/property-cell-DcsfnUNF.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
  import './shared/avatar-CzPZN3uP.js';
5
5
  import './shared/dates-a32DcW1l.js';
@@ -16,13 +16,20 @@ import './shared/dates-a32DcW1l.js';
16
16
  // <nk-database> the database pushes the data. A card fires nk-select
17
17
  // { row, id, value } on a click, Enter or Space, like a row of the table;
18
18
  // the add card nk-action { action: 'new-row' }.
19
+ // `href-key` (1.17.0) names the row field with the card's address: a row
20
+ // with one gets a link, <a class="nk-card" href>, in a .card-item that
21
+ // carries the list item's role – to open a course in a new tab or copy its
22
+ // address. A plain click, or Enter, fires the same nk-select; cancelling it
23
+ // keeps the browser from following the link. A middle click or one with
24
+ // Cmd, Ctrl, Shift or Alt fires nothing and does what a link does.
19
25
  // → <div class="nk-gallery" role="list"><div class="nk-card" role="listitem" tabindex="0">
20
26
  // <div class="nk-cover"><img src="…" alt=""></div><div class="card-title">🧭 …</div><div class="card-meta">…</div></div>
27
+ // <div class="card-item" role="listitem"><a class="nk-card" href="/courses/safety">…</a></div>
21
28
  // <div class="nk-new-row">+ New page</div></div>
22
29
  const SIZES = ['small', 'large'];
23
30
 
24
31
  class NkGalleryView extends NkElement {
25
- static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'cover-key', 'no-cover', 'size', 'fit', 'title-key', 'meta-keys', 'new-row', 'new-row-label']; }
32
+ 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']; }
26
33
 
27
34
  render() {
28
35
  this._grid = this.createElement('div', ['nk-gallery'], { role: 'list' });
@@ -48,9 +55,13 @@ class NkGalleryView extends NkElement {
48
55
  this._grid.className = ['nk-gallery', SIZES.includes(size) && size, this.getBoolAttr('fit') && 'fit'].filter(Boolean).join(' ');
49
56
  const title = this._titleColumn(), meta = this._metaColumns();
50
57
  const coverKey = this.getAttribute('cover-key') || 'cover', covers = !this.getBoolAttr('no-cover');
58
+ const hrefKey = this.getAttribute('href-key');
51
59
  this._grid.replaceChildren();
52
60
  for (const row of this._rows) {
53
- const card = this.createElement('div', ['nk-card'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
61
+ const link = hrefKey ? row[hrefKey] : null, href = link && typeof link === 'object' ? link.href : link;
62
+ const card = href
63
+ ? this.createElement('a', ['nk-card'], { href: String(href), 'data-id': row.id ?? '' })
64
+ : this.createElement('div', ['nk-card'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
54
65
  if (covers) {
55
66
  const cover = this.createElement('div', ['nk-cover']);
56
67
  if (row[coverKey]) cover.appendChild(this.createElement('img', [], { src: row[coverKey], alt: '' }));
@@ -63,17 +74,21 @@ class NkGalleryView extends NkElement {
63
74
  const v = row[c.key];
64
75
  if (v === undefined || v === null || v === '') continue;
65
76
  const s = document.createElement('span');
66
- if (c.type === 'progress') s.textContent = `▰ ${v}%`;
77
+ if (c.type === 'progress') s.textContent = `▰ ${formatPercent(c, v)}`;
67
78
  else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
68
79
  else s.appendChild(renderPropertyCell(c, v, row));
69
80
  m.appendChild(s);
70
81
  }
71
82
  card.append(t, m);
72
- this._grid.appendChild(card);
83
+ if (href) {
84
+ const item = this.createElement('div', ['card-item'], { role: 'listitem' });
85
+ item.appendChild(card);
86
+ this._grid.appendChild(item);
87
+ } else this._grid.appendChild(card);
73
88
  }
74
89
  if (this.getBoolAttr('new-row')) {
75
90
  const add = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0', 'data-add': '' });
76
- add.textContent = this.getAttribute('new-row-label') || '+ New page';
91
+ add.textContent = this.getAttribute('new-row-label') || this.str('newPage');
77
92
  this._grid.appendChild(add);
78
93
  }
79
94
  }
@@ -82,10 +97,16 @@ class NkGalleryView extends NkElement {
82
97
  this._onClick = (e) => {
83
98
  if (e.target.closest('[data-add]')) { this.emit('nk-action', { action: 'new-row' }); return; }
84
99
  const card = e.target.closest('.nk-card');
85
- if (card) { const row = this._rowById(card.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
100
+ if (!card) return;
101
+ // The link contract: a plain left click is a selection, any other one the browser's.
102
+ const link = card.localName === 'a';
103
+ if (link && (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)) return;
104
+ const row = this._rowById(card.dataset.id);
105
+ if (!this.emit('nk-select', { row, id: row?.id, value: row?.id }) && link) e.preventDefault();
86
106
  };
87
107
  this._onKey = (e) => {
88
108
  if (e.key !== 'Enter' && e.key !== ' ') return;
109
+ if (e.target.localName === 'a') return; // a link card: Enter is the browser's click, Space scrolls
89
110
  if (e.target.matches?.('.nk-card, [data-add]')) { e.preventDefault(); e.target.click(); }
90
111
  };
91
112
  this._grid.addEventListener('click', this._onClick);
@@ -99,6 +120,7 @@ class NkGalleryView extends NkElement {
99
120
 
100
121
  _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
101
122
  onAttributeChanged() { this._render(); }
123
+ onStringsChanged() { this._render(); }
102
124
 
103
125
  get name() { return this.getAttribute('name') || 'gallery'; }
104
126
  get columns() { return this._columns; }
@@ -21,7 +21,9 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
21
21
  // Events: nk-change { dataUrl, width, height, size } after a pick, and on
22
22
  // remove with an empty dataUrl; nk-error { message, name } when the
23
23
  // file cannot be decoded (HEIC outside Safari, a broken file).
24
- // Texts: choose-label, change-label, remove-label; label names the group.
24
+ // Texts: English or German after the language (1.17.0, setStrings() for
25
+ // others); choose-label, change-label, remove-label set them here.
26
+ // label names the group.
25
27
 
26
28
  function dataUrlBytes(url) {
27
29
  const b64 = url.slice(url.indexOf(',') + 1);
@@ -63,8 +65,8 @@ class NkImagePicker extends NkElement {
63
65
  }
64
66
  const label = this.getAttribute('label');
65
67
  if (label) this._row.setAttribute('aria-label', label); else this._row.removeAttribute('aria-label');
66
- this._choose.textContent = this._src ? (this.getAttribute('change-label') || 'Change image') : (this.getAttribute('choose-label') || 'Upload image');
67
- this._remove.textContent = this.getAttribute('remove-label') || 'Remove';
68
+ this._choose.textContent = this._src ? (this.getAttribute('change-label') || this.str('changeImage')) : (this.getAttribute('choose-label') || this.str('uploadImage'));
69
+ this._remove.textContent = this.getAttribute('remove-label') || this.str('remove');
68
70
  this._remove.hidden = !this._src;
69
71
  this._input.accept = this.getAttribute('accept') || 'image/*';
70
72
  }
@@ -112,6 +114,8 @@ class NkImagePicker extends NkElement {
112
114
  this._remove?.removeEventListener('click', this._onRemove);
113
115
  }
114
116
 
117
+ onStringsChanged() { this._sync(); }
118
+
115
119
  onAttributeChanged(name, _old, value) {
116
120
  if (name === 'src') this._src = value || '';
117
121
  this._sync();
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { a as textOf, f as formatDate, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
2
+ import { a as textOf, b as formatDate, r as renderPropertyCell } from './shared/property-cell-DcsfnUNF.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
  import './shared/avatar-CzPZN3uP.js';
5
5
  import './shared/dates-a32DcW1l.js';
@@ -56,7 +56,7 @@ class NkListView extends NkElement {
56
56
  item.append(icon, t, m);
57
57
  this._list.appendChild(item);
58
58
  }
59
- this._newRow.textContent = this.getAttribute('new-row-label') || '+ New page';
59
+ this._newRow.textContent = this.getAttribute('new-row-label') || this.str('newPage');
60
60
  this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
61
61
  }
62
62
 
@@ -81,6 +81,7 @@ class NkListView extends NkElement {
81
81
 
82
82
  _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
83
83
  onAttributeChanged() { this._render(); }
84
+ onStringsChanged() { this._render(); }
84
85
 
85
86
  get name() { return this.getAttribute('name') || 'list'; }
86
87
  get columns() { return this._columns; }
@@ -23,7 +23,7 @@ class NkModal extends NkElement {
23
23
  this._backdrop = this.createElement('div', ['nk-modal-backdrop']);
24
24
  this._backdrop.inert = true;
25
25
  this._box = this.createElement('div', ['nk-modal'], { role: 'dialog', 'aria-modal': 'true', tabindex: '-1' });
26
- this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': 'Settings' });
26
+ this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': this.str('settings') });
27
27
  this._nav.appendChild(this.createElement('slot', [], { name: 'user' }));
28
28
  this._rows = document.createElement('div');
29
29
  this._nav.appendChild(this._rows);
@@ -141,6 +141,8 @@ class NkModal extends NkElement {
141
141
  if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
142
142
  }
143
143
 
144
+ onStringsChanged() { this._nav.setAttribute('aria-label', this.str('settings')); }
145
+
144
146
  onAttributeChanged(name) {
145
147
  if (name === 'open') {
146
148
  this._syncOpen();
@@ -24,7 +24,7 @@ class NkPageTitle extends NkElement {
24
24
  this._h1.setAttribute('contenteditable', 'plaintext-only');
25
25
  this._h1.setAttribute('spellcheck', 'false');
26
26
  this._h1.setAttribute('role', 'textbox');
27
- this._h1.setAttribute('aria-label', 'Title');
27
+ this._h1.setAttribute('aria-label', this.str('title'));
28
28
  const text = this.getAttribute('value') ?? this.textContent.trim();
29
29
  if (this._h1.textContent !== text && document.activeElement !== this) this._h1.textContent = text;
30
30
  this._h1.setAttribute('data-placeholder', this.getAttribute('placeholder') || '');
@@ -59,6 +59,7 @@ class NkPageTitle extends NkElement {
59
59
  }
60
60
 
61
61
  onAttributeChanged() { this._sync(); }
62
+ onStringsChanged() { if (this._h1.hasAttribute('role')) this._h1.setAttribute('aria-label', this.str('title')); }
62
63
 
63
64
  get value() { return this.getBoolAttr('editable') ? (this._h1?.textContent.trim() ?? '') : (this.getAttribute('value') ?? this.textContent.trim()); }
64
65
  set value(v) { this.setAttribute('value', v); }
@@ -20,7 +20,7 @@ class NkPage extends NkElement {
20
20
 
21
21
  render() {
22
22
  this._page = this.createElement('div', ['nk-page']);
23
- this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: 'Change icon' });
23
+ this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: this.str('changeIcon') });
24
24
  this._iconSlot = this.createElement('slot', [], { name: 'icon' });
25
25
  this._iconSlot.appendChild(this._icon);
26
26
  this._page.appendChild(this._iconSlot);
@@ -76,6 +76,8 @@ class NkPage extends NkElement {
76
76
  this._iconSlot?.removeEventListener('slotchange', this._onCover);
77
77
  }
78
78
 
79
+ onStringsChanged() { this._icon.title = this.str('changeIcon'); }
80
+
79
81
  onAttributeChanged(name) {
80
82
  if (name === 'narrow') this._build();
81
83
  else this._sync();
@@ -27,7 +27,8 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
27
27
  // Notion: from `min` (380) to `max` (all but 320px of the window). The width
28
28
  // is the token --nk-peek-width, set on :root; `width` (px) sets it too, and
29
29
  // nk-resize { width } fires when a drag or a key ends – the moment to keep
30
- // it. resize-label names the edge (default "Resize").
30
+ // it. resize-label names the edge (default "Resize", in German "Breite
31
+ // ändern").
31
32
  // `inset`: while it is open on the desktop, the page's <nk-app> gets
32
33
  // `peek-inset` – its main column makes room instead of lying under the peek.
33
34
  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>';
@@ -74,7 +75,7 @@ class NkPeek extends NkElement {
74
75
 
75
76
  _syncResize() {
76
77
  this._handle.hidden = !this.getBoolAttr('resizable');
77
- this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || 'Resize');
78
+ this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || this.str('resize'));
78
79
  this._handle.setAttribute('aria-valuemin', this._min);
79
80
  this._handle.setAttribute('aria-valuenow', Math.round(this.width));
80
81
  }
@@ -91,7 +92,7 @@ class NkPeek extends NkElement {
91
92
  _syncLabels() {
92
93
  const label = this.getAttribute('label');
93
94
  if (label) this._box.setAttribute('aria-label', label); else this._box.removeAttribute('aria-label');
94
- this._close.setAttribute('aria-label', this.getAttribute('close-label') || 'Close');
95
+ this._close.setAttribute('aria-label', this.getAttribute('close-label') || this.str('close'));
95
96
  }
96
97
 
97
98
  _syncOpen() {
@@ -176,6 +177,8 @@ class NkPeek extends NkElement {
176
177
  this._wasOpen = false;
177
178
  }
178
179
 
180
+ onStringsChanged() { this._syncResize(); this._syncLabels(); }
181
+
179
182
  onAttributeChanged(name, _old, value) {
180
183
  if (name === 'width') { this._setWidth(Number(value)); return; }
181
184
  if (name === 'inset') { this._syncInset(); return; }
@@ -24,7 +24,8 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
24
24
  // brings it back; ⌘\ or Ctrl+\ toggles it (on a phone the drawer).
25
25
  // `collapsed` is the state, collapse() / expand() / toggleCollapsed() the
26
26
  // API, nk-collapse { collapsed } the moment to keep it. collapse-label names
27
- // the « (default "Close sidebar") and is its tooltip for <nk-tooltip>.
27
+ // the « (default "Close sidebar", in German "Seitenleiste schließen") and is
28
+ // its tooltip for <nk-tooltip>.
28
29
  const COLLAPSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m18 17-5-5 5-5M11 17l-5-5 5-5"/></svg>';
29
30
  const phone = () => matchMedia('(max-width: 860px)').matches;
30
31
 
@@ -54,7 +55,7 @@ class NkSidebar extends NkElement {
54
55
  }
55
56
 
56
57
  _syncCollapse() {
57
- const label = this.getAttribute('collapse-label') || 'Close sidebar';
58
+ const label = this.getAttribute('collapse-label') || this.str('closeSidebar');
58
59
  this._collapse.hidden = !this.getBoolAttr('collapsible');
59
60
  this._collapse.setAttribute('aria-label', label);
60
61
  this._collapse.dataset.tooltip = label;
@@ -106,6 +107,8 @@ class NkSidebar extends NkElement {
106
107
  document.removeEventListener('keydown', this._onKey);
107
108
  }
108
109
 
110
+ onStringsChanged() { this._syncCollapse(); }
111
+
109
112
  onAttributeChanged(name) {
110
113
  if (name === 'open') {
111
114
  this._syncOpen();