@jungherz-de/notionkit-elements 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +64 -6
- package/SKILL.md +79 -65
- package/dist/components/base.js +102 -3
- package/dist/components/nk-ai-input-row.js +6 -3
- package/dist/components/nk-board-view.js +2 -2
- package/dist/components/nk-breadcrumb.js +2 -1
- package/dist/components/nk-calendar-view.js +8 -5
- package/dist/components/nk-calendar.js +10 -7
- package/dist/components/nk-cmdk.js +6 -2
- package/dist/components/nk-comments.js +5 -3
- package/dist/components/nk-copy-field.js +37 -6
- package/dist/components/nk-emoji-picker.js +6 -2
- package/dist/components/nk-filter-bar.js +10 -8
- package/dist/components/nk-gallery-view.js +29 -7
- package/dist/components/nk-image-picker.js +7 -3
- package/dist/components/nk-list-view.js +3 -2
- package/dist/components/nk-modal.js +3 -1
- package/dist/components/nk-page-title.js +2 -1
- package/dist/components/nk-page.js +3 -1
- package/dist/components/nk-peek.js +6 -3
- package/dist/components/nk-progress.js +6 -3
- package/dist/components/nk-sidebar.js +5 -2
- package/dist/components/nk-steps.js +20 -4
- package/dist/components/nk-synced.js +2 -1
- package/dist/components/nk-tab-bar.js +10 -4
- package/dist/components/nk-table-view.js +3 -2
- package/dist/components/nk-theme-toggle.js +5 -2
- package/dist/components/nk-tree-item.js +3 -1
- package/dist/components/shared/{property-cell-BSVcETss.js → property-cell-DcsfnUNF.js} +15 -5
- package/dist/notionkit-elements.esm.js +312 -80
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +314 -79
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +32 -2
- package/src/components/content/nk-progress.js +6 -3
- package/src/components/data/nk-ai-input-row.js +6 -3
- package/src/components/data/nk-board-view.js +2 -2
- package/src/components/data/nk-calendar-view.js +7 -4
- package/src/components/data/nk-comments.js +5 -3
- package/src/components/data/nk-filter-bar.js +10 -8
- package/src/components/data/nk-gallery-view.js +29 -7
- package/src/components/data/nk-list-view.js +2 -1
- package/src/components/data/nk-table-view.js +2 -1
- package/src/components/forms/nk-calendar.js +10 -7
- package/src/components/forms/nk-copy-field.js +37 -6
- package/src/components/forms/nk-image-picker.js +7 -3
- package/src/components/overlays/nk-cmdk.js +6 -2
- package/src/components/overlays/nk-emoji-picker.js +6 -2
- package/src/components/overlays/nk-modal.js +3 -1
- package/src/components/overlays/nk-peek.js +6 -3
- package/src/components/page/nk-page-title.js +2 -1
- package/src/components/page/nk-page.js +3 -1
- package/src/components/page/nk-steps.js +20 -4
- package/src/components/page/nk-synced.js +2 -1
- package/src/components/shell/nk-breadcrumb.js +2 -1
- package/src/components/shell/nk-sidebar.js +5 -2
- package/src/components/shell/nk-tab-bar.js +10 -4
- package/src/components/shell/nk-theme-toggle.js +5 -2
- package/src/components/shell/nk-tree-item.js +3 -1
- package/src/index.js +5 -1
- package/src/util/property-cell.js +14 -4
- package/src/util/strings.js +73 -0
|
@@ -22,7 +22,7 @@ class NkModal extends NkElement {
|
|
|
22
22
|
this._backdrop = this.createElement('div', ['nk-modal-backdrop']);
|
|
23
23
|
this._backdrop.inert = true;
|
|
24
24
|
this._box = this.createElement('div', ['nk-modal'], { role: 'dialog', 'aria-modal': 'true', tabindex: '-1' });
|
|
25
|
-
this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': '
|
|
25
|
+
this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': this.str('settings') });
|
|
26
26
|
this._nav.appendChild(this.createElement('slot', [], { name: 'user' }));
|
|
27
27
|
this._rows = document.createElement('div');
|
|
28
28
|
this._nav.appendChild(this._rows);
|
|
@@ -140,6 +140,8 @@ class NkModal extends NkElement {
|
|
|
140
140
|
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
onStringsChanged() { this._nav.setAttribute('aria-label', this.str('settings')); }
|
|
144
|
+
|
|
143
145
|
onAttributeChanged(name) {
|
|
144
146
|
if (name === 'open') {
|
|
145
147
|
this._syncOpen();
|
|
@@ -26,7 +26,8 @@ import { lockScroll, unlockScroll, inertOutside, deepActiveElement } from '../..
|
|
|
26
26
|
// Notion: from `min` (380) to `max` (all but 320px of the window). The width
|
|
27
27
|
// is the token --nk-peek-width, set on :root; `width` (px) sets it too, and
|
|
28
28
|
// nk-resize { width } fires when a drag or a key ends – the moment to keep
|
|
29
|
-
// it. resize-label names the edge (default "Resize"
|
|
29
|
+
// it. resize-label names the edge (default "Resize", in German "Breite
|
|
30
|
+
// ändern").
|
|
30
31
|
// `inset`: while it is open on the desktop, the page's <nk-app> gets
|
|
31
32
|
// `peek-inset` – its main column makes room instead of lying under the peek.
|
|
32
33
|
const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 17 5-5-5-5M13 17l5-5-5-5"/></svg>';
|
|
@@ -73,7 +74,7 @@ class NkPeek extends NkElement {
|
|
|
73
74
|
|
|
74
75
|
_syncResize() {
|
|
75
76
|
this._handle.hidden = !this.getBoolAttr('resizable');
|
|
76
|
-
this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || '
|
|
77
|
+
this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || this.str('resize'));
|
|
77
78
|
this._handle.setAttribute('aria-valuemin', this._min);
|
|
78
79
|
this._handle.setAttribute('aria-valuenow', Math.round(this.width));
|
|
79
80
|
}
|
|
@@ -90,7 +91,7 @@ class NkPeek extends NkElement {
|
|
|
90
91
|
_syncLabels() {
|
|
91
92
|
const label = this.getAttribute('label');
|
|
92
93
|
if (label) this._box.setAttribute('aria-label', label); else this._box.removeAttribute('aria-label');
|
|
93
|
-
this._close.setAttribute('aria-label', this.getAttribute('close-label') || '
|
|
94
|
+
this._close.setAttribute('aria-label', this.getAttribute('close-label') || this.str('close'));
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
_syncOpen() {
|
|
@@ -175,6 +176,8 @@ class NkPeek extends NkElement {
|
|
|
175
176
|
this._wasOpen = false;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
onStringsChanged() { this._syncResize(); this._syncLabels(); }
|
|
180
|
+
|
|
178
181
|
onAttributeChanged(name, _old, value) {
|
|
179
182
|
if (name === 'width') { this._setWidth(Number(value)); return; }
|
|
180
183
|
if (name === 'inset') { this._syncInset(); return; }
|
|
@@ -23,7 +23,7 @@ class NkPageTitle extends NkElement {
|
|
|
23
23
|
this._h1.setAttribute('contenteditable', 'plaintext-only');
|
|
24
24
|
this._h1.setAttribute('spellcheck', 'false');
|
|
25
25
|
this._h1.setAttribute('role', 'textbox');
|
|
26
|
-
this._h1.setAttribute('aria-label', '
|
|
26
|
+
this._h1.setAttribute('aria-label', this.str('title'));
|
|
27
27
|
const text = this.getAttribute('value') ?? this.textContent.trim();
|
|
28
28
|
if (this._h1.textContent !== text && document.activeElement !== this) this._h1.textContent = text;
|
|
29
29
|
this._h1.setAttribute('data-placeholder', this.getAttribute('placeholder') || '');
|
|
@@ -58,6 +58,7 @@ class NkPageTitle extends NkElement {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
onAttributeChanged() { this._sync(); }
|
|
61
|
+
onStringsChanged() { if (this._h1.hasAttribute('role')) this._h1.setAttribute('aria-label', this.str('title')); }
|
|
61
62
|
|
|
62
63
|
get value() { return this.getBoolAttr('editable') ? (this._h1?.textContent.trim() ?? '') : (this.getAttribute('value') ?? this.textContent.trim()); }
|
|
63
64
|
set value(v) { this.setAttribute('value', v); }
|
|
@@ -19,7 +19,7 @@ class NkPage extends NkElement {
|
|
|
19
19
|
|
|
20
20
|
render() {
|
|
21
21
|
this._page = this.createElement('div', ['nk-page']);
|
|
22
|
-
this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: '
|
|
22
|
+
this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: this.str('changeIcon') });
|
|
23
23
|
this._iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
24
24
|
this._iconSlot.appendChild(this._icon);
|
|
25
25
|
this._page.appendChild(this._iconSlot);
|
|
@@ -75,6 +75,8 @@ class NkPage extends NkElement {
|
|
|
75
75
|
this._iconSlot?.removeEventListener('slotchange', this._onCover);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
onStringsChanged() { this._icon.title = this.str('changeIcon'); }
|
|
79
|
+
|
|
78
80
|
onAttributeChanged(name) {
|
|
79
81
|
if (name === 'narrow') this._build();
|
|
80
82
|
else this._sync();
|
|
@@ -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 =
|
|
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
|
|
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') ?? '
|
|
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': '
|
|
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"
|
|
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') || '
|
|
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();
|
|
@@ -13,8 +13,12 @@ import { NkElement } from '../../base.js';
|
|
|
13
13
|
// stylesheet hides it above 860px – there the sidebar is the navigation –
|
|
14
14
|
// and shows it below; `always` shows it at every width (previews, phone
|
|
15
15
|
// frames), `fixed` pins it to the viewport (standalone PWAs) with a spacer
|
|
16
|
-
// holding its place, `floating` makes it a capsule.
|
|
17
|
-
// `drawer` item opens the sidebar instead of
|
|
16
|
+
// holding its place, `floating` makes it a capsule. The item whose value is
|
|
17
|
+
// the bar's `value` is `active`; a `drawer` item opens the sidebar instead of
|
|
18
|
+
// becoming active when clicked. A value no item has – a page without a tab
|
|
19
|
+
// of its own, opened from the drawer – marks the drawer item, as iOS marks
|
|
20
|
+
// "More" (1.18.0); without one no item is active. Without a value the item
|
|
21
|
+
// marked `active`, else the first, is the one.
|
|
18
22
|
class NkTabBar extends NkElement {
|
|
19
23
|
static get observedAttributes() { return ['value', 'always', 'fixed', 'floating', 'label']; }
|
|
20
24
|
|
|
@@ -45,16 +49,18 @@ class NkTabBar extends NkElement {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
_sync() {
|
|
48
|
-
const items = this.items.filter(i => !i.hasAttribute('drawer'));
|
|
52
|
+
const items = this.items.filter(i => !i.hasAttribute('drawer')), drawers = this.items.filter(i => i.hasAttribute('drawer'));
|
|
49
53
|
if (!items.length) return;
|
|
50
54
|
let value = this.getAttribute('value');
|
|
51
|
-
if (value === null
|
|
55
|
+
if (value === null) {
|
|
52
56
|
const preset = items.find(i => i.hasAttribute('active')) || items[0];
|
|
53
57
|
value = this._valueOf(preset);
|
|
54
58
|
}
|
|
59
|
+
const known = items.some(i => this._valueOf(i) === value);
|
|
55
60
|
for (const item of items) {
|
|
56
61
|
if (this._valueOf(item) === value) item.setAttribute('active', ''); else item.removeAttribute('active');
|
|
57
62
|
}
|
|
63
|
+
drawers.forEach((item, n) => { if (!known && n === 0) item.setAttribute('active', ''); else item.removeAttribute('active'); });
|
|
58
64
|
this._value = value;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -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.
|
|
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.
|
|
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', '+', '
|
|
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',
|
|
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
|
+
}
|