@jungherz-de/notionkit-elements 1.15.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 (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +67 -7
  3. package/SKILL.md +67 -59
  4. package/dist/components/base.js +106 -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 +12 -9
  10. package/dist/components/nk-cmdk.js +7 -3
  11. package/dist/components/nk-comments.js +5 -3
  12. package/dist/components/nk-copy-field.js +6 -3
  13. package/dist/components/nk-dialog.js +1 -1
  14. package/dist/components/nk-emoji-picker.js +6 -2
  15. package/dist/components/nk-filter-bar.js +10 -8
  16. package/dist/components/nk-gallery-view.js +29 -7
  17. package/dist/components/nk-image-picker.js +7 -3
  18. package/dist/components/nk-list-view.js +3 -2
  19. package/dist/components/nk-menu.js +2 -2
  20. package/dist/components/nk-modal.js +4 -2
  21. package/dist/components/nk-page-title.js +2 -1
  22. package/dist/components/nk-page.js +3 -1
  23. package/dist/components/nk-panel.js +4 -1
  24. package/dist/components/nk-peek.js +7 -4
  25. package/dist/components/nk-select.js +13 -8
  26. package/dist/components/nk-sheet.js +1 -1
  27. package/dist/components/nk-sidebar.js +6 -3
  28. package/dist/components/nk-steps.js +20 -4
  29. package/dist/components/nk-synced.js +2 -1
  30. package/dist/components/nk-table-view.js +3 -2
  31. package/dist/components/nk-theme-toggle.js +5 -2
  32. package/dist/components/nk-tooltip.js +1 -1
  33. package/dist/components/nk-tree-item.js +3 -1
  34. package/dist/components/shared/{focus-BNAChOXO.js → focus-C4tbSNND.js} +7 -2
  35. package/dist/components/shared/{property-cell-BSVcETss.js → property-cell-DcsfnUNF.js} +15 -5
  36. package/dist/notionkit-elements.esm.js +295 -83
  37. package/dist/notionkit-elements.esm.min.js +2 -0
  38. package/dist/notionkit-elements.esm.min.js.map +1 -0
  39. package/dist/notionkit-elements.js +297 -82
  40. package/dist/notionkit-elements.min.js +1 -1
  41. package/package.json +4 -3
  42. package/src/base.js +36 -2
  43. package/src/components/data/nk-ai-input-row.js +6 -3
  44. package/src/components/data/nk-board-view.js +2 -2
  45. package/src/components/data/nk-calendar-view.js +7 -4
  46. package/src/components/data/nk-comments.js +5 -3
  47. package/src/components/data/nk-filter-bar.js +10 -8
  48. package/src/components/data/nk-gallery-view.js +29 -7
  49. package/src/components/data/nk-list-view.js +2 -1
  50. package/src/components/data/nk-table-view.js +2 -1
  51. package/src/components/forms/nk-calendar.js +11 -8
  52. package/src/components/forms/nk-copy-field.js +6 -3
  53. package/src/components/forms/nk-image-picker.js +7 -3
  54. package/src/components/forms/nk-select.js +13 -8
  55. package/src/components/overlays/nk-cmdk.js +6 -2
  56. package/src/components/overlays/nk-emoji-picker.js +6 -2
  57. package/src/components/overlays/nk-menu.js +1 -1
  58. package/src/components/overlays/nk-modal.js +3 -1
  59. package/src/components/overlays/nk-peek.js +6 -3
  60. package/src/components/page/nk-page-title.js +2 -1
  61. package/src/components/page/nk-page.js +3 -1
  62. package/src/components/page/nk-panel.js +4 -1
  63. package/src/components/page/nk-steps.js +20 -4
  64. package/src/components/page/nk-synced.js +2 -1
  65. package/src/components/shell/nk-breadcrumb.js +2 -1
  66. package/src/components/shell/nk-sidebar.js +5 -2
  67. package/src/components/shell/nk-theme-toggle.js +5 -2
  68. package/src/components/shell/nk-tree-item.js +3 -1
  69. package/src/index.js +5 -1
  70. package/src/util/focus.js +7 -2
  71. package/src/util/property-cell.js +14 -4
  72. package/src/util/strings.js +73 -0
@@ -1,5 +1,79 @@
1
1
  import { tokensCss, componentsSheet } from '@jungherz-de/notionkit/notionkit-styles.js';
2
2
 
3
+ // The texts the elements show and announce by themselves – button labels,
4
+ // placeholders, accessible names – in English and German (NotionKit
5
+ // Elements 1.17.0). The language is the element's own: the nearest `lang`
6
+ // attribute, across shadow roots, else <html lang>; German for "de…",
7
+ // English for anything else. setStrings() adds or replaces texts, for one
8
+ // language or for all of them, and an attribute on the element wins over
9
+ // every one of these.
10
+ const BUILT_IN = {
11
+ en: {
12
+ copy: 'Copy', copied: 'Copied', show: 'Show', hide: 'Hide',
13
+ uploadImage: 'Upload image', changeImage: 'Change image', remove: 'Remove',
14
+ close: 'Close', resize: 'Resize',
15
+ today: 'Today', previousMonth: 'Previous month', nextMonth: 'Next month', clear: 'Clear', time: 'Time',
16
+ filter: 'Filter', sort: 'Sort', addFilter: '+ Filter', removeFilter: 'Remove filter', search: 'Search …',
17
+ closeSidebar: 'Close sidebar', newPage: '+ New page',
18
+ commentPlaceholder: 'Comment …', send: 'Send', askPlaceholder: 'Ask something …',
19
+ commandPlaceholder: 'Search or type a command …', emojiPlaceholder: 'Search…',
20
+ toggleTheme: 'Toggle light / dark', synced: '⟳ synced',
21
+ message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
22
+ searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
23
+ add: 'Add', more: 'More',
24
+ },
25
+ de: {
26
+ copy: 'Kopieren', copied: 'Kopiert', show: 'Zeigen', hide: 'Verbergen',
27
+ uploadImage: 'Bild hochladen', changeImage: 'Bild ändern', remove: 'Entfernen',
28
+ close: 'Schließen', resize: 'Breite ändern',
29
+ today: 'Heute', previousMonth: 'Voriger Monat', nextMonth: 'Nächster Monat', clear: 'Leeren', time: 'Uhrzeit',
30
+ filter: 'Filter', sort: 'Sortieren', addFilter: '+ Filter', removeFilter: 'Filter entfernen', search: 'Suchen …',
31
+ closeSidebar: 'Seitenleiste schließen', newPage: '+ Neue Seite',
32
+ commentPlaceholder: 'Kommentieren …', send: 'Senden', askPlaceholder: 'Frag etwas …',
33
+ commandPlaceholder: 'Suchen oder Befehl eingeben …', emojiPlaceholder: 'Suchen …',
34
+ toggleTheme: 'Hell / Dunkel', synced: '⟳ synchronisiert',
35
+ message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
36
+ searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
37
+ add: 'Hinzufügen', more: 'Mehr',
38
+ },
39
+ };
40
+ const custom = {};
41
+ const listeners = new Set();
42
+
43
+ /**
44
+ * Adds or replaces texts: setStrings({ copy: 'Copy link' }) for every
45
+ * language, setStrings({ copy: 'Copier' }, 'fr') for one – a language of its
46
+ * own too. The elements on the page take them at once.
47
+ */
48
+ function setStrings(strings, lang = '*') {
49
+ const key = String(lang).toLowerCase().split('-')[0];
50
+ custom[key] = { ...custom[key], ...strings };
51
+ for (const fn of listeners) fn();
52
+ }
53
+
54
+ /** The keys and texts built in for a language ('en' or 'de'), for an app that fills in its own. */
55
+ function builtInStrings(lang = 'en') {
56
+ return { ...(BUILT_IN[String(lang).toLowerCase().split('-')[0]] || BUILT_IN.en) };
57
+ }
58
+
59
+ /** Called after every setStrings(); base.js hands the change on to the elements. */
60
+ function onStringsChange(fn) { listeners.add(fn); }
61
+
62
+ /** The language of `el`: its nearest lang attribute, across shadow roots, else the page's; the primary subtag. */
63
+ function langOf(el) {
64
+ for (let node = el; node; node = node.getRootNode?.().host) {
65
+ const hit = node.closest?.('[lang]');
66
+ if (hit) return hit.getAttribute('lang').toLowerCase().split('-')[0];
67
+ }
68
+ return (document.documentElement.lang || 'en').toLowerCase().split('-')[0];
69
+ }
70
+
71
+ /** The text `key` for `el`: its language's own set by setStrings(), then the one for every language, the built-in one, English. */
72
+ function str(key, el) {
73
+ const lang = langOf(el);
74
+ return custom[lang]?.[key] ?? custom['*']?.[key] ?? BUILT_IN[lang]?.[key] ?? BUILT_IN.en[key] ?? key;
75
+ }
76
+
3
77
  // ============================================================
4
78
  // NotionKit Elements – base classes
5
79
  //
@@ -50,10 +124,20 @@ function syncAllThemes() {
50
124
  for (const instance of instances) instance._syncTheme(theme);
51
125
  }
52
126
 
127
+ // The built-in texts follow the language (1.17.0): setStrings() and a new
128
+ // lang on <html> reach the elements already on the page, too.
129
+ function syncAllStrings() {
130
+ for (const instance of instances) instance.onStringsChanged();
131
+ }
132
+ onStringsChange(syncAllStrings);
133
+
53
134
  if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
54
- new MutationObserver(syncAllThemes).observe(document.documentElement, {
135
+ new MutationObserver(records => {
136
+ if (records.some(r => r.attributeName === 'data-theme')) syncAllThemes();
137
+ if (records.some(r => r.attributeName === 'lang')) syncAllStrings();
138
+ }).observe(document.documentElement, {
55
139
  attributes: true,
56
- attributeFilter: ['data-theme']
140
+ attributeFilter: ['data-theme', 'lang']
57
141
  });
58
142
  }
59
143
 
@@ -61,6 +145,9 @@ if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
61
145
  // Layout plumbing only: how the host box participates in the outer layout, and
62
146
  // that `hidden` keeps working. :host([hidden]) has a higher specificity than
63
147
  // :host, so it wins regardless of order. The wrapper is layout-transparent.
148
+ // `flush` on any element drops the outer margin of its box (NotionKit
149
+ // 1.16.0) – for a block in a flex column with a gap of its own; a whole
150
+ // column does it for every block with --nk-block-space: 0.
64
151
 
65
152
  const hostSheets = new Map();
66
153
 
@@ -71,6 +158,7 @@ function hostSheetFor(display) {
71
158
  sheet.replaceSync(`
72
159
  :host { display: ${display}; }
73
160
  :host([hidden]) { display: none; }
161
+ :host([flush]) .nk-wrapper > * { margin-top: 0; margin-bottom: 0; }
74
162
  .nk-wrapper { display: contents; }
75
163
  `);
76
164
  hostSheets.set(display, sheet);
@@ -194,6 +282,21 @@ class NkElement extends HTMLElement {
194
282
  this._wrapper?.setAttribute('data-theme', theme);
195
283
  }
196
284
 
285
+ /**
286
+ * The text `key` in this element's language – its nearest lang attribute,
287
+ * across shadow roots, else the page's: the one setStrings() gave, else the
288
+ * built-in English or German one (1.17.0). A project's own element reads
289
+ * its texts here too, after setStrings({ myKey: '…' }).
290
+ */
291
+ str(key) { return str(key, this); }
292
+
293
+ /**
294
+ * The texts may have changed: setStrings() ran or <html lang> changed.
295
+ * Components with built-in texts apply them again here; an attribute on
296
+ * the element still wins.
297
+ */
298
+ onStringsChanged() {}
299
+
197
300
  _upgradeOwnProperties() {
198
301
  for (const key of Object.keys(this)) {
199
302
  if (key.startsWith('_')) continue;
@@ -343,4 +446,4 @@ class NkFormElement extends NkElement {
343
446
  }
344
447
  }
345
448
 
346
- export { NkElement, NkFormElement, getCurrentTheme };
449
+ export { NkElement, NkFormElement, builtInStrings, getCurrentTheme, setStrings };
@@ -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();
@@ -1,7 +1,7 @@
1
1
  import { NkFormElement } from './base.js';
2
2
  import { p as parseDay, i as isoOf, C as CHEVRONS, r as resolveLocale, f as firstWeekday, m as monthGrid, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth, c as addDays } from './shared/dates-a32DcW1l.js';
3
3
  import { p as placeUnder } from './shared/floating-DOK73b_D.js';
4
- import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
4
+ import { d as deepActiveElement } from './shared/focus-C4tbSNND.js';
5
5
  import '@jungherz-de/notionkit/notionkit-styles.js';
6
6
 
7
7
  // <nk-calendar name="due" value="2026-06-02" weeks weekend="6,0"></nk-calendar>
@@ -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.
@@ -44,7 +45,7 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
44
45
  // outside does. Put a floating calendar directly under <body>.
45
46
  const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
46
47
  const floatSheet = new CSSStyleSheet();
47
- floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none; }`);
48
+ floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 106; pointer-events: none; }`);
48
49
 
49
50
  class NkCalendar extends NkFormElement {
50
51
  static get hostStyles() { return floatSheet; }
@@ -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; }
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { l as lockScroll, i as inertOutside, u as unlockScroll } from './shared/focus-BNAChOXO.js';
2
+ import { l as lockScroll, i as inertOutside, u as unlockScroll } from './shared/focus-C4tbSNND.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
 
5
5
  // Subsequence scoring for the command palette: every query character must
@@ -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();
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-BNAChOXO.js';
2
+ import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-C4tbSNND.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
 
5
5
  // <nk-dialog id="trash" title="Move “Roadmap” to trash?" alert>
@@ -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; }