@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
@@ -17,6 +17,12 @@ import { NkElement } from '../../base.js';
17
17
  // nk-select { index, value, step } – index counts from 1, like `current` –
18
18
  // and, unless cancelled, makes that step current. `horizontal` sets the
19
19
  // steps in one row above a wizard.
20
+ // A step with `href` (1.17.0) shows its label as a link, <a class="st-label">,
21
+ // with or without `selectable` – to open a chapter 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, for a router of your own. A
24
+ // middle click or one with Cmd, Ctrl, Shift or Alt fires nothing and does
25
+ // what a link does.
20
26
  class NkSteps extends NkElement {
21
27
  static get observedAttributes() { return ['steps', 'current', 'label', 'selectable', 'horizontal']; }
22
28
 
@@ -44,7 +50,7 @@ class NkSteps extends NkElement {
44
50
  // Rebuilding drops the focused label; the new one at that place takes it back.
45
51
  const focused = this.shadowRoot?.activeElement?.closest?.('li')?.dataset.index;
46
52
  this._list.replaceChildren(...this._items().map((_, i) => {
47
- const n = i + 1, { label: text, desc, state } = this._step(i);
53
+ const n = i + 1, { label: text, desc, state, href } = this._step(i);
48
54
  // A state of its own wins; without one, the steps before `current` are done.
49
55
  const done = state ? state === 'done' : n < current, skipped = state === 'skipped';
50
56
  const li = this.createElement('li', ['nk-step'], { 'data-index': String(n) });
@@ -54,7 +60,8 @@ class NkSteps extends NkElement {
54
60
  if (n === current) li.setAttribute('aria-current', 'step');
55
61
  const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
56
62
  mark.textContent = done ? '✓' : skipped ? '–' : String(n);
57
- const body = selectable ? this.createElement('button', ['st-label'], { type: 'button' }) : document.createElement('span');
63
+ const body = href != null && href !== '' ? this.createElement('a', ['st-label'], { href: String(href) })
64
+ : selectable ? this.createElement('button', ['st-label'], { type: 'button' }) : document.createElement('span');
58
65
  body.textContent = text ?? '';
59
66
  if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
60
67
  li.append(mark, body);
@@ -65,9 +72,18 @@ class NkSteps extends NkElement {
65
72
 
66
73
  setupEvents() {
67
74
  this._onClick = (e) => {
68
- if (!this.getBoolAttr('selectable')) return;
69
75
  const li = e.target.closest?.('li.nk-step');
70
- if (li && this._list.contains(li)) this.select(Number(li.dataset.index));
76
+ if (!li || !this._list.contains(li)) return;
77
+ const link = li.querySelector('a.st-label');
78
+ if (link) {
79
+ // The link contract: a plain left click is a selection, any other
80
+ // one the browser's. The mark beside the link counts as the link.
81
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
82
+ if (!link.contains(e.target)) { link.click(); return; }
83
+ if (!this.select(Number(li.dataset.index))) e.preventDefault();
84
+ return;
85
+ }
86
+ if (this.getBoolAttr('selectable')) this.select(Number(li.dataset.index));
71
87
  };
72
88
  this._list.addEventListener('click', this._onClick);
73
89
  }
@@ -14,8 +14,9 @@ class NkSynced extends NkElement {
14
14
  this._sync();
15
15
  }
16
16
 
17
- _sync() { this._badge.textContent = this.getAttribute('badge') ?? '⟳ synced'; }
17
+ _sync() { this._badge.textContent = this.getAttribute('badge') ?? this.str('synced'); }
18
18
  onAttributeChanged() { this._sync(); }
19
+ onStringsChanged() { this._sync(); }
19
20
 
20
21
  get badge() { return this.getAttribute('badge'); }
21
22
  set badge(v) { this.setAttribute('badge', v); }
@@ -16,7 +16,7 @@ class NkBreadcrumb extends NkElement {
16
16
  static get observesLightDom() { return true; }
17
17
 
18
18
  render() {
19
- this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': 'Breadcrumb' });
19
+ this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': this.str('breadcrumb') });
20
20
  this._wrapper.appendChild(this._nav);
21
21
  this.projectLightDom();
22
22
  }
@@ -68,6 +68,7 @@ class NkBreadcrumb extends NkElement {
68
68
  teardownEvents() { this._nav?.removeEventListener('click', this._onClick); }
69
69
 
70
70
  onAttributeChanged() { this.projectLightDom(); }
71
+ onStringsChanged() { this._nav.setAttribute('aria-label', this.str('breadcrumb')); }
71
72
 
72
73
  get separator() { return this.getAttribute('separator') ?? '/'; }
73
74
  set separator(v) { this.setAttribute('separator', v); }
@@ -23,7 +23,8 @@ import { deepActiveElement } from '../../util/focus.js';
23
23
  // brings it back; ⌘\ or Ctrl+\ toggles it (on a phone the drawer).
24
24
  // `collapsed` is the state, collapse() / expand() / toggleCollapsed() the
25
25
  // API, nk-collapse { collapsed } the moment to keep it. collapse-label names
26
- // the « (default "Close sidebar") and is its tooltip for <nk-tooltip>.
26
+ // the « (default "Close sidebar", in German "Seitenleiste schließen") and is
27
+ // its tooltip for <nk-tooltip>.
27
28
  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>';
28
29
  const phone = () => matchMedia('(max-width: 860px)').matches;
29
30
 
@@ -53,7 +54,7 @@ class NkSidebar extends NkElement {
53
54
  }
54
55
 
55
56
  _syncCollapse() {
56
- const label = this.getAttribute('collapse-label') || 'Close sidebar';
57
+ const label = this.getAttribute('collapse-label') || this.str('closeSidebar');
57
58
  this._collapse.hidden = !this.getBoolAttr('collapsible');
58
59
  this._collapse.setAttribute('aria-label', label);
59
60
  this._collapse.dataset.tooltip = label;
@@ -105,6 +106,8 @@ class NkSidebar extends NkElement {
105
106
  document.removeEventListener('keydown', this._onKey);
106
107
  }
107
108
 
109
+ onStringsChanged() { this._syncCollapse(); }
110
+
108
111
  onAttributeChanged(name) {
109
112
  if (name === 'open') {
110
113
  this._syncOpen();
@@ -14,7 +14,7 @@ class NkThemeToggle extends NkElement {
14
14
 
15
15
  render() {
16
16
  this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
17
- this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
17
+ this._syncTitle();
18
18
  this._wrapper.appendChild(this._btn);
19
19
  const root = document.documentElement;
20
20
  let stored = null;
@@ -45,8 +45,11 @@ class NkThemeToggle extends NkElement {
45
45
  window.removeEventListener('message', this._onMessage);
46
46
  }
47
47
 
48
+ _syncTitle() { this._btn.title = this.getAttribute('title') || this.str('toggleTheme'); }
49
+ onStringsChanged() { this._syncTitle(); }
50
+
48
51
  onAttributeChanged(name) {
49
- if (name === 'title' && this._btn) this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
52
+ if (name === 'title' && this._btn) this._syncTitle();
50
53
  }
51
54
 
52
55
  /** Sets the theme on <html>, persists it and reports it. */
@@ -33,7 +33,7 @@ class NkTreeItem extends NkElement {
33
33
  this._labelSlot.appendChild(this._labelText);
34
34
  this._label.appendChild(this._labelSlot);
35
35
  this._actions = this.createElement('span', ['actions']);
36
- for (const [action, glyph, title] of [['add', '+', 'Add'], ['more', '⋯', 'More']]) {
36
+ for (const [action, glyph, title] of [['add', '+', this.str('add')], ['more', '⋯', this.str('more')]]) {
37
37
  const b = this.createElement('span', [], { 'data-action': action, role: 'button', title });
38
38
  b.textContent = glyph;
39
39
  this._actions.appendChild(b);
@@ -119,6 +119,8 @@ class NkTreeItem extends NkElement {
119
119
  this._row?.removeEventListener('keydown', this._onKey);
120
120
  }
121
121
 
122
+ onStringsChanged() { for (const b of this._actions.children) b.title = this.str(b.dataset.action); }
123
+
122
124
  onAttributeChanged(name) {
123
125
  if (name === 'label') this.assignSlots();
124
126
  else this._sync();
package/src/index.js CHANGED
@@ -117,4 +117,8 @@ export { NkComment } from './components/data/nk-comment.js';
117
117
  export { NkAiThread } from './components/data/nk-ai-thread.js';
118
118
  export { NkAiMsg } from './components/data/nk-ai-msg.js';
119
119
  export { NkAiInputRow } from './components/data/nk-ai-input-row.js';
120
- export { renderPropertyCell, tagFor, compareBy, formatDate, timeOf, textOf } from './util/property-cell.js';
120
+ export { renderPropertyCell, tagFor, compareBy, formatDate, formatPercent, timeOf, textOf } from './util/property-cell.js';
121
+
122
+ // 1.17.0 – the built-in texts in English and German, after the lang of the
123
+ // page or of the element; setStrings() adds or replaces them.
124
+ export { setStrings, builtInStrings } from './util/strings.js';
@@ -13,6 +13,8 @@
13
13
  // text: a string, or { text, desc, color, tooltip, sort } – one of Notion's
14
14
  // nine text colours for a tone, a quiet second line under it, the whole
15
15
  // text in a tooltip (served by <nk-tooltip>), a value to sort by.
16
+ // progress: 0–100, the bar and its percentage as Intl writes it in
17
+ // column.locale, else the page's language ("45 %" in German).
16
18
  // date: the value as given, or with column.format ('short', 'relative' or
17
19
  // Intl.DateTimeFormat options) and column.locale formatted from an ISO date
18
20
  // or date-time, D.M.YYYY, a Date or a timestamp; { start, end } is a range.
@@ -80,7 +82,7 @@ export function renderPropertyCell(column, value, row = {}) {
80
82
  if (type === 'actions') return actions(column, value);
81
83
  if (value === undefined || value === null || value === '') {
82
84
  if (type === 'checkbox') return checkbox(column, false, row);
83
- if (type === 'progress') return progress(0);
85
+ if (type === 'progress') return progress(column, 0);
84
86
  return el('span', type === 'person' ? 'person-cell' : null, type === 'text' && column.title ? '' : '—');
85
87
  }
86
88
  switch (type) {
@@ -112,7 +114,7 @@ export function renderPropertyCell(column, value, row = {}) {
112
114
  }
113
115
  // column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
114
116
  case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
115
- case 'progress': return progress(Number(value) || 0);
117
+ case 'progress': return progress(column, Number(value) || 0);
116
118
  default: {
117
119
  const o = typeof value === 'object' ? value : { text: value };
118
120
  let t;
@@ -158,15 +160,23 @@ function checkbox(column, checked, row) {
158
160
  return label;
159
161
  }
160
162
 
161
- function progress(value) {
163
+ function progress(column, value) {
162
164
  const frag = document.createDocumentFragment();
163
165
  const bar = el('span', 'nk-progress');
164
166
  const fill = el('i'); fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
165
167
  bar.appendChild(fill);
166
- frag.append(bar, el('span', 'nk-progress-label', `${Math.round(value)}%`));
168
+ frag.append(bar, el('span', 'nk-progress-label', formatPercent(column, value)));
167
169
  return frag;
168
170
  }
169
171
 
172
+ /** A progress value of 0–100 as Intl writes a percentage in column.locale, else the page's language: 45 → "45%", in German "45 %". */
173
+ export function formatPercent(column, value) {
174
+ const n = Number(value) || 0;
175
+ try {
176
+ return new Intl.NumberFormat(column.locale || resolveLocale(), { style: 'percent', maximumFractionDigits: 0 }).format(n / 100);
177
+ } catch { return `${Math.round(n)}%`; } // a lang Intl does not take
178
+ }
179
+
170
180
  /** A select's place among its options: the order Notion sorts by. */
171
181
  function rankOf(column, value) {
172
182
  const i = (column.options || []).findIndex(o => o.value === value || o.label === value);
@@ -0,0 +1,73 @@
1
+ // The texts the elements show and announce by themselves – button labels,
2
+ // placeholders, accessible names – in English and German (NotionKit
3
+ // Elements 1.17.0). The language is the element's own: the nearest `lang`
4
+ // attribute, across shadow roots, else <html lang>; German for "de…",
5
+ // English for anything else. setStrings() adds or replaces texts, for one
6
+ // language or for all of them, and an attribute on the element wins over
7
+ // every one of these.
8
+ const BUILT_IN = {
9
+ en: {
10
+ copy: 'Copy', copied: 'Copied', show: 'Show', hide: 'Hide',
11
+ uploadImage: 'Upload image', changeImage: 'Change image', remove: 'Remove',
12
+ close: 'Close', resize: 'Resize',
13
+ today: 'Today', previousMonth: 'Previous month', nextMonth: 'Next month', clear: 'Clear', time: 'Time',
14
+ filter: 'Filter', sort: 'Sort', addFilter: '+ Filter', removeFilter: 'Remove filter', search: 'Search …',
15
+ closeSidebar: 'Close sidebar', newPage: '+ New page',
16
+ commentPlaceholder: 'Comment …', send: 'Send', askPlaceholder: 'Ask something …',
17
+ commandPlaceholder: 'Search or type a command …', emojiPlaceholder: 'Search…',
18
+ toggleTheme: 'Toggle light / dark', synced: '⟳ synced',
19
+ message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
20
+ searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
21
+ add: 'Add', more: 'More',
22
+ },
23
+ de: {
24
+ copy: 'Kopieren', copied: 'Kopiert', show: 'Zeigen', hide: 'Verbergen',
25
+ uploadImage: 'Bild hochladen', changeImage: 'Bild ändern', remove: 'Entfernen',
26
+ close: 'Schließen', resize: 'Breite ändern',
27
+ today: 'Heute', previousMonth: 'Voriger Monat', nextMonth: 'Nächster Monat', clear: 'Leeren', time: 'Uhrzeit',
28
+ filter: 'Filter', sort: 'Sortieren', addFilter: '+ Filter', removeFilter: 'Filter entfernen', search: 'Suchen …',
29
+ closeSidebar: 'Seitenleiste schließen', newPage: '+ Neue Seite',
30
+ commentPlaceholder: 'Kommentieren …', send: 'Senden', askPlaceholder: 'Frag etwas …',
31
+ commandPlaceholder: 'Suchen oder Befehl eingeben …', emojiPlaceholder: 'Suchen …',
32
+ toggleTheme: 'Hell / Dunkel', synced: '⟳ synchronisiert',
33
+ message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
34
+ searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
35
+ add: 'Hinzufügen', more: 'Mehr',
36
+ },
37
+ };
38
+ const custom = {};
39
+ const listeners = new Set();
40
+
41
+ /**
42
+ * Adds or replaces texts: setStrings({ copy: 'Copy link' }) for every
43
+ * language, setStrings({ copy: 'Copier' }, 'fr') for one – a language of its
44
+ * own too. The elements on the page take them at once.
45
+ */
46
+ export function setStrings(strings, lang = '*') {
47
+ const key = String(lang).toLowerCase().split('-')[0];
48
+ custom[key] = { ...custom[key], ...strings };
49
+ for (const fn of listeners) fn();
50
+ }
51
+
52
+ /** The keys and texts built in for a language ('en' or 'de'), for an app that fills in its own. */
53
+ export function builtInStrings(lang = 'en') {
54
+ return { ...(BUILT_IN[String(lang).toLowerCase().split('-')[0]] || BUILT_IN.en) };
55
+ }
56
+
57
+ /** Called after every setStrings(); base.js hands the change on to the elements. */
58
+ export function onStringsChange(fn) { listeners.add(fn); }
59
+
60
+ /** The language of `el`: its nearest lang attribute, across shadow roots, else the page's; the primary subtag. */
61
+ export function langOf(el) {
62
+ for (let node = el; node; node = node.getRootNode?.().host) {
63
+ const hit = node.closest?.('[lang]');
64
+ if (hit) return hit.getAttribute('lang').toLowerCase().split('-')[0];
65
+ }
66
+ return (document.documentElement.lang || 'en').toLowerCase().split('-')[0];
67
+ }
68
+
69
+ /** The text `key` for `el`: its language's own set by setStrings(), then the one for every language, the built-in one, English. */
70
+ export function str(key, el) {
71
+ const lang = langOf(el);
72
+ return custom[lang]?.[key] ?? custom['*']?.[key] ?? BUILT_IN[lang]?.[key] ?? BUILT_IN.en[key] ?? key;
73
+ }