@jungherz-de/notionkit-elements 1.14.0 → 1.16.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 +37 -0
- package/README.md +9 -7
- package/SKILL.md +33 -31
- package/dist/components/base.js +4 -0
- package/dist/components/nk-board-view.js +4 -3
- package/dist/components/nk-calendar-view.js +3 -1
- package/dist/components/nk-calendar.js +2 -2
- package/dist/components/nk-cmdk.js +1 -1
- package/dist/components/nk-dialog.js +1 -1
- package/dist/components/nk-filter-bar.js +3 -2
- package/dist/components/nk-gallery-view.js +4 -3
- package/dist/components/nk-list-view.js +4 -3
- package/dist/components/nk-menu.js +2 -2
- package/dist/components/nk-modal.js +1 -1
- package/dist/components/nk-panel.js +4 -1
- package/dist/components/nk-peek.js +1 -1
- package/dist/components/nk-select.js +13 -8
- package/dist/components/nk-sheet.js +1 -1
- package/dist/components/nk-sidebar.js +1 -1
- package/dist/components/nk-table-view.js +2 -1
- package/dist/components/nk-tooltip.js +1 -1
- package/dist/components/shared/{focus-BNAChOXO.js → focus-C4tbSNND.js} +7 -2
- package/dist/components/shared/property-cell-BSVcETss.js +204 -0
- package/dist/notionkit-elements.esm.js +132 -37
- package/dist/notionkit-elements.esm.min.js +2 -0
- package/dist/notionkit-elements.esm.min.js.map +1 -0
- package/dist/notionkit-elements.js +134 -36
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +4 -3
- package/src/base.js +4 -0
- package/src/components/data/nk-board-view.js +3 -3
- package/src/components/data/nk-calendar-view.js +2 -1
- package/src/components/data/nk-filter-bar.js +3 -2
- package/src/components/data/nk-gallery-view.js +3 -3
- package/src/components/data/nk-list-view.js +3 -3
- package/src/components/forms/nk-calendar.js +1 -1
- package/src/components/forms/nk-select.js +13 -8
- package/src/components/overlays/nk-menu.js +1 -1
- package/src/components/page/nk-panel.js +4 -1
- package/src/index.js +1 -1
- package/src/util/focus.js +7 -2
- package/src/util/property-cell.js +90 -12
- package/dist/components/shared/property-cell-DKvv6TIG.js +0 -126
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"import": "./dist/notionkit-elements.esm.js",
|
|
21
21
|
"default": "./dist/notionkit-elements.js"
|
|
22
22
|
},
|
|
23
|
+
"./esm.min.js": "./dist/notionkit-elements.esm.min.js",
|
|
23
24
|
"./base.js": "./dist/components/base.js",
|
|
24
25
|
"./components/*.js": "./dist/components/*.js",
|
|
25
26
|
"./package.json": "./package.json"
|
|
@@ -62,10 +63,10 @@
|
|
|
62
63
|
"dark-mode"
|
|
63
64
|
],
|
|
64
65
|
"peerDependencies": {
|
|
65
|
-
"@jungherz-de/notionkit": ">=1.
|
|
66
|
+
"@jungherz-de/notionkit": ">=1.16.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@jungherz-de/notionkit": "^1.
|
|
69
|
+
"@jungherz-de/notionkit": "^1.16.0",
|
|
69
70
|
"@playwright/test": "^1.50.0",
|
|
70
71
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
71
72
|
"@rollup/plugin-terser": "^0.4.0",
|
package/src/base.js
CHANGED
|
@@ -60,6 +60,9 @@ if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
|
|
|
60
60
|
// Layout plumbing only: how the host box participates in the outer layout, and
|
|
61
61
|
// that `hidden` keeps working. :host([hidden]) has a higher specificity than
|
|
62
62
|
// :host, so it wins regardless of order. The wrapper is layout-transparent.
|
|
63
|
+
// `flush` on any element drops the outer margin of its box (NotionKit
|
|
64
|
+
// 1.16.0) – for a block in a flex column with a gap of its own; a whole
|
|
65
|
+
// column does it for every block with --nk-block-space: 0.
|
|
63
66
|
|
|
64
67
|
const hostSheets = new Map();
|
|
65
68
|
|
|
@@ -70,6 +73,7 @@ function hostSheetFor(display) {
|
|
|
70
73
|
sheet.replaceSync(`
|
|
71
74
|
:host { display: ${display}; }
|
|
72
75
|
:host([hidden]) { display: none; }
|
|
76
|
+
:host([flush]) .nk-wrapper > * { margin-top: 0; margin-bottom: 0; }
|
|
73
77
|
.nk-wrapper { display: contents; }
|
|
74
78
|
`);
|
|
75
79
|
hostSheets.set(display, sheet);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
-
import { renderPropertyCell, tagFor } from '../../util/property-cell.js';
|
|
2
|
+
import { renderPropertyCell, tagFor, textOf, formatDate } from '../../util/property-cell.js';
|
|
3
3
|
|
|
4
4
|
// <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
|
|
5
5
|
// Groups rows by a select column (default: the first select column); one
|
|
@@ -49,14 +49,14 @@ class NkBoardView extends NkElement {
|
|
|
49
49
|
for (const row of cards) {
|
|
50
50
|
const card = this.createElement('div', ['nk-card'], { draggable: 'true', 'data-id': row.id ?? '', tabindex: '0' });
|
|
51
51
|
const t = this.createElement('div', ['card-title']);
|
|
52
|
-
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? row[title.key]
|
|
52
|
+
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? textOf(row[title.key]) : ''}`;
|
|
53
53
|
const meta = this.createElement('div', ['card-meta']);
|
|
54
54
|
for (const c of this._metaColumns()) {
|
|
55
55
|
const v = row[c.key];
|
|
56
56
|
if (v === undefined || v === null || v === '') continue;
|
|
57
57
|
const s = document.createElement('span');
|
|
58
58
|
if (c.type === 'progress') s.textContent = `▰ ${v}%`;
|
|
59
|
-
else if (c.type === 'date') s.textContent = `📅 ${v}`;
|
|
59
|
+
else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
|
|
60
60
|
else s.appendChild(renderPropertyCell(c, v, row));
|
|
61
61
|
meta.appendChild(s);
|
|
62
62
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { textOf } from '../../util/property-cell.js';
|
|
2
3
|
import { isoOf, parseDay, isoWeek, resolveLocale, firstWeekday, weekdayNames, monthGrid, stepMonth, weekLabel, CHEVRONS } from '../../util/dates.js';
|
|
3
4
|
|
|
4
5
|
// <nk-calendar-view name="calendar" label="📅 Calendar" date-key="due" weeks></nk-calendar-view>
|
|
@@ -82,7 +83,7 @@ class NkCalendarView extends NkElement {
|
|
|
82
83
|
day.appendChild(num);
|
|
83
84
|
for (const row of byDay[iso] || []) {
|
|
84
85
|
const item = this.createElement('button', ['cv-item'], { type: 'button', 'data-id': row.id ?? '' });
|
|
85
|
-
const title = titleCol ?
|
|
86
|
+
const title = titleCol ? textOf(row[titleCol.key]) : '';
|
|
86
87
|
item.textContent = row.icon ? `${row.icon} ${title}` : title;
|
|
87
88
|
day.appendChild(item);
|
|
88
89
|
}
|
|
@@ -87,12 +87,13 @@ class NkFilterBar extends NkElement {
|
|
|
87
87
|
/**
|
|
88
88
|
* Applies filters and search to rows: a filter keeps rows where row[key]
|
|
89
89
|
* equals value – or differs from it with op: 'is-not'; the search looks
|
|
90
|
-
* for the text in every string field (a person's name
|
|
90
|
+
* for the text in every string field (a person's name, a text or link
|
|
91
|
+
* object's text).
|
|
91
92
|
*/
|
|
92
93
|
apply(rows) {
|
|
93
94
|
const q = this.value.trim().toLowerCase();
|
|
94
95
|
return rows.filter(r => this._filters.every(f => (f.op === 'is-not' ? r[f.key] !== f.value : r[f.key] === f.value))
|
|
95
|
-
&& (!q || Object.values(r).some(v => String(typeof v === 'object' && v ? v.name ?? '' : v ?? '').toLowerCase().includes(q))));
|
|
96
|
+
&& (!q || Object.values(r).some(v => String(typeof v === 'object' && v ? v.name ?? v.text ?? v.label ?? '' : v ?? '').toLowerCase().includes(q))));
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
-
import { renderPropertyCell } from '../../util/property-cell.js';
|
|
2
|
+
import { renderPropertyCell, textOf, formatDate } from '../../util/property-cell.js';
|
|
3
3
|
|
|
4
4
|
// <nk-gallery-view name="gallery" label="🖼 Gallery" meta-keys="status,due" new-row></nk-gallery-view>
|
|
5
5
|
// The fifth database view (NotionKit 1.14.0): the rows as cards with a
|
|
@@ -54,14 +54,14 @@ class NkGalleryView extends NkElement {
|
|
|
54
54
|
card.appendChild(cover);
|
|
55
55
|
}
|
|
56
56
|
const t = this.createElement('div', ['card-title']);
|
|
57
|
-
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? row[title.key]
|
|
57
|
+
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? textOf(row[title.key]) : ''}`;
|
|
58
58
|
const m = this.createElement('div', ['card-meta']);
|
|
59
59
|
for (const c of meta) {
|
|
60
60
|
const v = row[c.key];
|
|
61
61
|
if (v === undefined || v === null || v === '') continue;
|
|
62
62
|
const s = document.createElement('span');
|
|
63
63
|
if (c.type === 'progress') s.textContent = `▰ ${v}%`;
|
|
64
|
-
else if (c.type === 'date') s.textContent = `📅 ${v}`;
|
|
64
|
+
else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
|
|
65
65
|
else s.appendChild(renderPropertyCell(c, v, row));
|
|
66
66
|
m.appendChild(s);
|
|
67
67
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
-
import { renderPropertyCell } from '../../util/property-cell.js';
|
|
2
|
+
import { renderPropertyCell, textOf, formatDate } from '../../util/property-cell.js';
|
|
3
3
|
|
|
4
4
|
// <nk-list-view name="list" label="☰ List" meta-keys="due,status" new-row></nk-list-view>
|
|
5
5
|
// The third database view: one line per row – icon and title, the `meta-keys`
|
|
@@ -43,12 +43,12 @@ class NkListView extends NkElement {
|
|
|
43
43
|
icon.textContent = row.icon || '';
|
|
44
44
|
if (!row.icon) icon.style.display = 'none';
|
|
45
45
|
const t = this.createElement('span', ['l-title']);
|
|
46
|
-
t.textContent = title ?
|
|
46
|
+
t.textContent = title ? textOf(row[title.key]) : '';
|
|
47
47
|
const m = this.createElement('span', ['l-meta']);
|
|
48
48
|
for (const c of meta) {
|
|
49
49
|
const v = row[c.key];
|
|
50
50
|
if (v === undefined || v === null || v === '') continue;
|
|
51
|
-
m.appendChild(c.type === 'date'
|
|
51
|
+
m.appendChild(c.type === 'date' ? document.createTextNode(formatDate(c, v)) : c.type === 'text' || !c.type ? document.createTextNode(textOf(v)) : renderPropertyCell(c, v, row));
|
|
52
52
|
}
|
|
53
53
|
item.append(icon, t, m);
|
|
54
54
|
this._list.appendChild(item);
|
|
@@ -43,7 +43,7 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
43
43
|
// outside does. Put a floating calendar directly under <body>.
|
|
44
44
|
const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
45
45
|
const floatSheet = new CSSStyleSheet();
|
|
46
|
-
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index:
|
|
46
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 106; pointer-events: none; }`);
|
|
47
47
|
|
|
48
48
|
class NkCalendar extends NkFormElement {
|
|
49
49
|
static get hostStyles() { return floatSheet; }
|
|
@@ -8,8 +8,10 @@ import { NkFormElement } from '../../base.js';
|
|
|
8
8
|
// The light-DOM <option>s are copied into the shadow <select>. A
|
|
9
9
|
// MutationObserver keeps the copy in step when a framework swaps them; the
|
|
10
10
|
// empty string is a valid value; the live selection survives a rebuild. A
|
|
11
|
-
// value
|
|
12
|
-
//
|
|
11
|
+
// value – attribute or property – whose option is not there yet waits and
|
|
12
|
+
// wins over the browser's preselection once the option arrives, unless
|
|
13
|
+
// someone has chosen another option in the meantime; settling on it fires
|
|
14
|
+
// no nk-change.
|
|
13
15
|
class NkSelect extends NkFormElement {
|
|
14
16
|
static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'wide', 'aria-label']; }
|
|
15
17
|
static get observesLightDom() { return true; }
|
|
@@ -19,6 +21,7 @@ class NkSelect extends NkFormElement {
|
|
|
19
21
|
this._forwardAll();
|
|
20
22
|
this._wrapper.appendChild(this._select);
|
|
21
23
|
this._defaultValue = this.getAttribute('value');
|
|
24
|
+
this._pending = this._defaultValue;
|
|
22
25
|
// Children may not be parsed yet when connectedCallback runs.
|
|
23
26
|
this.projectLightDom();
|
|
24
27
|
requestAnimationFrame(() => this.projectLightDom());
|
|
@@ -52,11 +55,11 @@ class NkSelect extends NkFormElement {
|
|
|
52
55
|
this._select.innerHTML = '';
|
|
53
56
|
for (const node of nodes) this._select.appendChild(node.cloneNode(true));
|
|
54
57
|
|
|
55
|
-
// A value that
|
|
56
|
-
//
|
|
57
|
-
// default (first option / `selected`).
|
|
58
|
-
if (this._pending != null
|
|
59
|
-
else
|
|
58
|
+
// A value that waits for its option first – it keeps waiting while the
|
|
59
|
+
// option is missing; else the live selection when it survived; else the
|
|
60
|
+
// browser default (first option / `selected`).
|
|
61
|
+
if (this._pending != null) { if (this._applyValue(this._pending)) this._pending = null; }
|
|
62
|
+
else this._applyValue(previous);
|
|
60
63
|
this._syncFormValue();
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -85,7 +88,8 @@ class NkSelect extends NkFormElement {
|
|
|
85
88
|
onAttributeChanged(name) {
|
|
86
89
|
if (!this._select) return;
|
|
87
90
|
if (name === 'value') {
|
|
88
|
-
this.
|
|
91
|
+
const value = this.getAttribute('value');
|
|
92
|
+
this._pending = value === null || this._applyValue(value) ? null : value;
|
|
89
93
|
this._syncFormValue();
|
|
90
94
|
return;
|
|
91
95
|
}
|
|
@@ -99,6 +103,7 @@ class NkSelect extends NkFormElement {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
resetValue() {
|
|
106
|
+
this._pending = null;
|
|
102
107
|
if (!this._applyValue(this._defaultValue)) this._select.selectedIndex = 0;
|
|
103
108
|
this._syncFormValue();
|
|
104
109
|
}
|
|
@@ -19,7 +19,7 @@ import { placeUnder } from '../../util/floating.js';
|
|
|
19
19
|
// close it too, a switch row does not. Opened from the keyboard, focus moves
|
|
20
20
|
// to the first item; closing returns it to where it was.
|
|
21
21
|
const floatSheet = new CSSStyleSheet();
|
|
22
|
-
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index:
|
|
22
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 106; pointer-events: none; }`);
|
|
23
23
|
|
|
24
24
|
class NkMenu extends NkElement {
|
|
25
25
|
static get hostStyles() { return floatSheet; }
|
|
@@ -42,7 +42,10 @@ class NkPanel extends NkElement {
|
|
|
42
42
|
const href = this.getAttribute('href');
|
|
43
43
|
if (href) { this._box.setAttribute('href', href); this._box.target = this.getAttribute('target') || ''; }
|
|
44
44
|
const cover = this.getAttribute('cover');
|
|
45
|
-
|
|
45
|
+
// The band is in the tree only with `cover`: hidden, it still matched
|
|
46
|
+
// `.nk-cover + .nk-page-icon` and pulled a lone icon over the tile's edge.
|
|
47
|
+
if (cover === null) this._cover.remove();
|
|
48
|
+
else if (!this._cover.isConnected) this._box.prepend(this._cover);
|
|
46
49
|
if (cover) { this._img.src = cover; if (!this._img.isConnected) this._cover.appendChild(this._img); }
|
|
47
50
|
else this._img.remove();
|
|
48
51
|
const icon = this.getAttribute('icon');
|
package/src/index.js
CHANGED
|
@@ -117,4 +117,4 @@ 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 } from './util/property-cell.js';
|
|
120
|
+
export { renderPropertyCell, tagFor, compareBy, formatDate, timeOf, textOf } from './util/property-cell.js';
|
package/src/util/focus.js
CHANGED
|
@@ -15,7 +15,12 @@ export function unlockScroll() {
|
|
|
15
15
|
if (locks > 0 && --locks === 0) document.documentElement.style.overflow = previousOverflow;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Floating layers under <body> that open from inside an overlay – a menu, a
|
|
19
|
+
// date picker – and the tooltip and toast stay usable: made inert, a date
|
|
20
|
+
// picker opened from a dialog showed but took no click.
|
|
21
|
+
const LAYERS = 'nk-menu[floating], nk-calendar[floating], nk-tooltip, nk-toast';
|
|
22
|
+
|
|
23
|
+
/** Makes every sibling along `el`'s ancestor chain inert (shadow hosts included), floating layers aside; returns the undo. */
|
|
19
24
|
export function inertOutside(el) {
|
|
20
25
|
const made = [];
|
|
21
26
|
let node = el;
|
|
@@ -23,7 +28,7 @@ export function inertOutside(el) {
|
|
|
23
28
|
const parent = node.parentNode instanceof ShadowRoot ? node.parentNode : node.parentNode;
|
|
24
29
|
if (!parent) break;
|
|
25
30
|
for (const sib of parent.children) {
|
|
26
|
-
if (sib === node || sib.inert || /^(SCRIPT|STYLE|LINK|TEMPLATE)$/.test(sib.tagName)) continue;
|
|
31
|
+
if (sib === node || sib.inert || /^(SCRIPT|STYLE|LINK|TEMPLATE)$/.test(sib.tagName) || sib.matches(LAYERS)) continue;
|
|
27
32
|
sib.inert = true;
|
|
28
33
|
made.push(sib);
|
|
29
34
|
}
|
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
// actions: buttons for the row from column.actions [{ action, label, icon,
|
|
11
11
|
// danger, disabled, tooltip }]; the row's value – action names or objects –
|
|
12
12
|
// picks which of them it shows, all of them without one.
|
|
13
|
+
// text: a string, or { text, desc, color, tooltip, sort } – one of Notion's
|
|
14
|
+
// nine text colours for a tone, a quiet second line under it, the whole
|
|
15
|
+
// text in a tooltip (served by <nk-tooltip>), a value to sort by.
|
|
16
|
+
// date: the value as given, or with column.format ('short', 'relative' or
|
|
17
|
+
// Intl.DateTimeFormat options) and column.locale formatted from an ISO date
|
|
18
|
+
// or date-time, D.M.YYYY, a Date or a timestamp; { start, end } is a range.
|
|
19
|
+
// Sorting (compareBy) goes by column.sortKey – another field of the row –
|
|
20
|
+
// when there is one, else by a value's `sort`, a date's time, a select's
|
|
21
|
+
// place among its options; empty cells come last in both directions.
|
|
13
22
|
import { paintAvatar, initialsOf } from './avatar.js';
|
|
23
|
+
import { resolveLocale } from './dates.js';
|
|
14
24
|
|
|
15
25
|
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
16
26
|
const COLORS = ['gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red'];
|
|
@@ -26,6 +36,45 @@ export function tagFor(column, value) {
|
|
|
26
36
|
return el('span', `nk-tag ${color}`, opt?.label ?? String(value));
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
/** The text of a value: a string as it is, a text, link or person object's text, label or name. */
|
|
40
|
+
export const textOf = v => (v && typeof v === 'object' ? v.text ?? v.label ?? v.name ?? '' : String(v ?? ''));
|
|
41
|
+
|
|
42
|
+
/** The time of a date value in ms: an ISO date – local midnight – or date-time, D.M.YYYY, a Date, a timestamp, a range's start; NaN when it is none. */
|
|
43
|
+
export function timeOf(v) {
|
|
44
|
+
if (v instanceof Date) return v.getTime();
|
|
45
|
+
if (typeof v === 'number') return v;
|
|
46
|
+
if (v && typeof v === 'object') return timeOf(v.start);
|
|
47
|
+
const s = String(v ?? '').trim();
|
|
48
|
+
let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
49
|
+
if (m) return new Date(+m[3], +m[2] - 1, +m[1]).getTime();
|
|
50
|
+
m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
51
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3]).getTime();
|
|
52
|
+
return /^\d{4}-\d{2}-\d{2}T/.test(s) ? Date.parse(s) : NaN;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A date as its column shows it: as given without column.format; 'short' –
|
|
57
|
+
* day and month, the year when it is not this one; 'relative' – minutes or
|
|
58
|
+
* hours ago or ahead within a day, else the short date; or the options of
|
|
59
|
+
* Intl.DateTimeFormat. A range { start, end } stands as start → end.
|
|
60
|
+
*/
|
|
61
|
+
export function formatDate(column, value) {
|
|
62
|
+
if (value && typeof value === 'object' && !(value instanceof Date)) return [value.start, value.end].filter(Boolean).map(v => formatDate(column, v)).join(' → ');
|
|
63
|
+
const format = column.format, t = timeOf(value);
|
|
64
|
+
if (!format || Number.isNaN(t)) return String(value ?? '');
|
|
65
|
+
const d = new Date(t), locale = column.locale || resolveLocale();
|
|
66
|
+
if (format === 'relative') {
|
|
67
|
+
const minutes = Math.round((t - Date.now()) / 60000), rel = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });
|
|
68
|
+
if (Math.abs(minutes) < 60) return rel.format(minutes, 'minute');
|
|
69
|
+
if (Math.abs(minutes) < 24 * 60) return rel.format(Math.round(minutes / 60), 'hour');
|
|
70
|
+
}
|
|
71
|
+
if (format === 'short' || format === 'relative') {
|
|
72
|
+
const year = d.getFullYear() === new Date().getFullYear() ? {} : { year: 'numeric' };
|
|
73
|
+
return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', ...year });
|
|
74
|
+
}
|
|
75
|
+
return typeof format === 'object' ? new Intl.DateTimeFormat(locale, format).format(d) : String(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
29
78
|
export function renderPropertyCell(column, value, row = {}) {
|
|
30
79
|
const type = column.type || 'text';
|
|
31
80
|
if (type === 'actions') return actions(column, value);
|
|
@@ -42,7 +91,7 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
42
91
|
for (const v of Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean)) wrap.appendChild(tagFor(column, v));
|
|
43
92
|
return wrap;
|
|
44
93
|
}
|
|
45
|
-
case 'date': return el('span', 'date-cell',
|
|
94
|
+
case 'date': return el('span', 'date-cell', formatDate(column, value));
|
|
46
95
|
case 'person': {
|
|
47
96
|
const cell = el('span', 'person-cell');
|
|
48
97
|
const p = typeof value === 'string' ? { name: value } : value;
|
|
@@ -65,13 +114,20 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
65
114
|
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
|
|
66
115
|
case 'progress': return progress(Number(value) || 0);
|
|
67
116
|
default: {
|
|
117
|
+
const o = typeof value === 'object' ? value : { text: value };
|
|
118
|
+
let t;
|
|
68
119
|
if (column.title) {
|
|
69
|
-
|
|
120
|
+
t = el('span', 'row-title');
|
|
70
121
|
if (row.icon) t.append(document.createTextNode(row.icon + ' '));
|
|
71
|
-
t.append(document.createTextNode(String(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return el('span', null, String(value));
|
|
122
|
+
t.append(document.createTextNode(String(o.text ?? '')));
|
|
123
|
+
} else if (typeof value === 'object') {
|
|
124
|
+
t = el('span', o.color && COLORS.includes(o.color) ? `td-text ${o.color}` : 'td-text', String(o.text ?? ''));
|
|
125
|
+
} else return el('span', null, String(value));
|
|
126
|
+
if (o.tooltip) t.dataset.tooltip = o.tooltip;
|
|
127
|
+
if (!o.desc) return t;
|
|
128
|
+
const frag = document.createDocumentFragment();
|
|
129
|
+
frag.append(t, el('span', 'td-desc', String(o.desc)));
|
|
130
|
+
return frag;
|
|
75
131
|
}
|
|
76
132
|
}
|
|
77
133
|
}
|
|
@@ -111,13 +167,35 @@ function progress(value) {
|
|
|
111
167
|
return frag;
|
|
112
168
|
}
|
|
113
169
|
|
|
114
|
-
/**
|
|
170
|
+
/** A select's place among its options: the order Notion sorts by. */
|
|
171
|
+
function rankOf(column, value) {
|
|
172
|
+
const i = (column.options || []).findIndex(o => o.value === value || o.label === value);
|
|
173
|
+
return i < 0 ? (column.options || []).length : i;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** What a column sorts a row by – see the head of this file. */
|
|
177
|
+
function sortValue(column, row) {
|
|
178
|
+
if (column.sortKey) return row[column.sortKey];
|
|
179
|
+
const v = row[column.key];
|
|
180
|
+
if (v === undefined || v === null || v === '') return null;
|
|
181
|
+
if (typeof v === 'object' && !Array.isArray(v) && 'sort' in v) return v.sort;
|
|
182
|
+
switch (column.type || 'text') {
|
|
183
|
+
case 'number': case 'progress': return Number(v);
|
|
184
|
+
case 'checkbox': return v ? 1 : 0;
|
|
185
|
+
case 'date': { const t = timeOf(v); return Number.isNaN(t) ? String(v) : t; }
|
|
186
|
+
case 'select': return rankOf(column, v);
|
|
187
|
+
case 'multi-select': { const first = (Array.isArray(v) ? v : String(v).split(','))[0]; return first == null ? null : rankOf(column, String(first).trim()); }
|
|
188
|
+
default: return textOf(v);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Sort comparator for a column; empty cells come last whichever the direction. */
|
|
115
193
|
export function compareBy(column, dir = 1) {
|
|
116
|
-
const
|
|
194
|
+
const empty = v => v === null || v === undefined || v === '' || Number.isNaN(v);
|
|
117
195
|
return (a, b) => {
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
121
|
-
return String(x
|
|
196
|
+
const x = sortValue(column, a), y = sortValue(column, b);
|
|
197
|
+
if (empty(x) || empty(y)) return empty(x) === empty(y) ? 0 : empty(x) ? 1 : -1;
|
|
198
|
+
if (typeof x === 'number' && typeof y === 'number') return (x - y) * dir;
|
|
199
|
+
return String(x).localeCompare(String(y), undefined, { numeric: true }) * dir;
|
|
122
200
|
};
|
|
123
201
|
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { i as initialsOf, p as paintAvatar } from './avatar-CzPZN3uP.js';
|
|
2
|
-
|
|
3
|
-
// Polymorphic property renderer for database views. Returns plain DOM built
|
|
4
|
-
// from NotionKit classes – deliberately not a custom element: every cell rule
|
|
5
|
-
// starts with `.nk-table`, so a cell inside its own shadow root would never
|
|
6
|
-
// be styled. Exported for consumers who render tables themselves.
|
|
7
|
-
//
|
|
8
|
-
// column: { key, label, type, icon, options: [{ value, label, color }], title }
|
|
9
|
-
// types: text | select | multi-select | date | person | checkbox | url | number | progress | actions
|
|
10
|
-
// url: a string is an address outside, opened in a new tab; { href, label,
|
|
11
|
-
// target } a link of your own – to another page of the app, with its text.
|
|
12
|
-
// actions: buttons for the row from column.actions [{ action, label, icon,
|
|
13
|
-
// danger, disabled, tooltip }]; the row's value – action names or objects –
|
|
14
|
-
// picks which of them it shows, all of them without one.
|
|
15
|
-
|
|
16
|
-
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
17
|
-
const COLORS = ['gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red'];
|
|
18
|
-
|
|
19
|
-
function optionFor(column, value) {
|
|
20
|
-
const opts = column.options || [];
|
|
21
|
-
return opts.find(o => o.value === value) || opts.find(o => o.label === value) || null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function tagFor(column, value) {
|
|
25
|
-
const opt = optionFor(column, value);
|
|
26
|
-
const color = opt?.color && COLORS.includes(opt.color) ? opt.color : 'gray'; // Notion's default option colour
|
|
27
|
-
return el('span', `nk-tag ${color}`, opt?.label ?? String(value));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function renderPropertyCell(column, value, row = {}) {
|
|
31
|
-
const type = column.type || 'text';
|
|
32
|
-
if (type === 'actions') return actions(column, value);
|
|
33
|
-
if (value === undefined || value === null || value === '') {
|
|
34
|
-
if (type === 'checkbox') return checkbox(column, false, row);
|
|
35
|
-
if (type === 'progress') return progress(0);
|
|
36
|
-
return el('span', type === 'person' ? 'person-cell' : null, type === 'text' && column.title ? '' : '—');
|
|
37
|
-
}
|
|
38
|
-
switch (type) {
|
|
39
|
-
case 'select': return tagFor(column, value);
|
|
40
|
-
case 'multi-select': {
|
|
41
|
-
const wrap = el('span');
|
|
42
|
-
wrap.style.display = 'inline-flex'; wrap.style.gap = '4px'; wrap.style.flexWrap = 'wrap';
|
|
43
|
-
for (const v of Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean)) wrap.appendChild(tagFor(column, v));
|
|
44
|
-
return wrap;
|
|
45
|
-
}
|
|
46
|
-
case 'date': return el('span', 'date-cell', String(value));
|
|
47
|
-
case 'person': {
|
|
48
|
-
const cell = el('span', 'person-cell');
|
|
49
|
-
const p = typeof value === 'string' ? { name: value } : value;
|
|
50
|
-
// color: one of Notion's nine names, or any CSS background; none – the avatar gradient.
|
|
51
|
-
const avatar = el('span', 'nk-avatar small', p.initials || p.avatar || initialsOf(p.name));
|
|
52
|
-
paintAvatar(avatar, p.color);
|
|
53
|
-
cell.append(avatar, document.createTextNode(' ' + (p.name || '')));
|
|
54
|
-
return cell;
|
|
55
|
-
}
|
|
56
|
-
case 'checkbox': return checkbox(column, !!value, row);
|
|
57
|
-
case 'url': {
|
|
58
|
-
const link = typeof value === 'object' ? value : { href: String(value), label: String(value).replace(/^https?:\/\//, ''), target: '_blank' };
|
|
59
|
-
const a = el('a', null, link.label ?? link.href);
|
|
60
|
-
a.href = link.href ?? '';
|
|
61
|
-
if (link.target) a.target = link.target;
|
|
62
|
-
if (link.target === '_blank') a.rel = 'noopener';
|
|
63
|
-
return a;
|
|
64
|
-
}
|
|
65
|
-
// column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
|
|
66
|
-
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
|
|
67
|
-
case 'progress': return progress(Number(value) || 0);
|
|
68
|
-
default: {
|
|
69
|
-
if (column.title) {
|
|
70
|
-
const t = el('span', 'row-title');
|
|
71
|
-
if (row.icon) t.append(document.createTextNode(row.icon + ' '));
|
|
72
|
-
t.append(document.createTextNode(String(value)));
|
|
73
|
-
return t;
|
|
74
|
-
}
|
|
75
|
-
return el('span', null, String(value));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function actions(column, value) {
|
|
81
|
-
const all = column.actions || [];
|
|
82
|
-
const list = value == null ? all
|
|
83
|
-
: (Array.isArray(value) ? value : [value]).map(v => typeof v === 'string' ? all.find(a => a.action === v) ?? { action: v, label: v } : v);
|
|
84
|
-
const wrap = el('span', 'row-actions');
|
|
85
|
-
for (const a of list) {
|
|
86
|
-
const b = el('button', `nk-btn ${a.danger ? 'danger' : 'secondary'} small`, [a.icon, a.label ?? a.action].filter(Boolean).join(' '));
|
|
87
|
-
b.type = 'button';
|
|
88
|
-
b.dataset.action = a.action;
|
|
89
|
-
b.disabled = !!a.disabled;
|
|
90
|
-
if (a.tooltip) b.dataset.tooltip = a.tooltip;
|
|
91
|
-
wrap.appendChild(b);
|
|
92
|
-
}
|
|
93
|
-
return wrap;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function checkbox(column, checked, row) {
|
|
97
|
-
const label = el('label', 'nk-check');
|
|
98
|
-
const input = el('input');
|
|
99
|
-
input.type = 'checkbox'; input.checked = checked;
|
|
100
|
-
input.dataset.key = column.key;
|
|
101
|
-
if (row.id != null) input.dataset.rowId = row.id;
|
|
102
|
-
label.appendChild(input);
|
|
103
|
-
return label;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function progress(value) {
|
|
107
|
-
const frag = document.createDocumentFragment();
|
|
108
|
-
const bar = el('span', 'nk-progress');
|
|
109
|
-
const fill = el('i'); fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
|
110
|
-
bar.appendChild(fill);
|
|
111
|
-
frag.append(bar, el('span', 'nk-progress-label', `${Math.round(value)}%`));
|
|
112
|
-
return frag;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Sort comparator for a column type. */
|
|
116
|
-
function compareBy(column, dir = 1) {
|
|
117
|
-
const type = column.type || 'text';
|
|
118
|
-
return (a, b) => {
|
|
119
|
-
let x = a[column.key], y = b[column.key];
|
|
120
|
-
if (type === 'person') { x = x?.name ?? x ?? ''; y = y?.name ?? y ?? ''; }
|
|
121
|
-
if (type === 'number' || type === 'progress' || type === 'checkbox') return ((Number(x) || 0) - (Number(y) || 0)) * dir;
|
|
122
|
-
return String(x ?? '').localeCompare(String(y ?? ''), undefined, { numeric: true }) * dir;
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export { compareBy as c, renderPropertyCell as r, tagFor as t };
|