@jungherz-de/notionkit-elements 1.8.0 → 1.10.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 (32) hide show
  1. package/README.md +13 -13
  2. package/dist/components/nk-app.js +8 -1
  3. package/dist/components/nk-btn.js +17 -3
  4. package/dist/components/nk-calendar-view.js +138 -0
  5. package/dist/components/nk-calendar.js +356 -0
  6. package/dist/components/nk-dialog.js +150 -0
  7. package/dist/components/nk-menu.js +2 -14
  8. package/dist/components/nk-peek.js +80 -5
  9. package/dist/components/nk-sidebar.js +68 -6
  10. package/dist/components/nk-table-view.js +9 -3
  11. package/dist/components/nk-tooltip.js +138 -0
  12. package/dist/components/nk-workspace-switcher.js +1 -1
  13. package/dist/components/shared/dates-a32DcW1l.js +67 -0
  14. package/dist/components/shared/floating-CxHxy-Cb.js +37 -0
  15. package/dist/notionkit-elements.esm.js +1102 -91
  16. package/dist/notionkit-elements.js +1105 -90
  17. package/dist/notionkit-elements.min.js +1 -1
  18. package/package.json +3 -3
  19. package/src/components/data/nk-calendar-view.js +136 -0
  20. package/src/components/data/nk-table-view.js +9 -3
  21. package/src/components/forms/nk-btn.js +17 -3
  22. package/src/components/forms/nk-calendar.js +354 -0
  23. package/src/components/overlays/nk-dialog.js +148 -0
  24. package/src/components/overlays/nk-menu.js +2 -14
  25. package/src/components/overlays/nk-peek.js +80 -5
  26. package/src/components/overlays/nk-tooltip.js +136 -0
  27. package/src/components/shell/nk-app.js +8 -1
  28. package/src/components/shell/nk-sidebar.js +68 -6
  29. package/src/components/shell/nk-workspace-switcher.js +1 -1
  30. package/src/index.js +4 -0
  31. package/src/util/dates.js +65 -0
  32. package/src/util/floating.js +35 -0
@@ -21,17 +21,28 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
21
21
  // the peek) leave it open. Below 860px it is a bottom sheet over a dimmed
22
22
  // page; there it is modal: the page is inert and scroll-locked, the dimmed
23
23
  // page takes the tap that closes it. Put it directly under <body>.
24
+ //
25
+ // `resizable` (NotionKit 1.10.0): the left edge takes the pointer – and the
26
+ // arrow keys, 16px a step – to make the peek wider or narrower, as in
27
+ // Notion: from `min` (380) to `max` (all but 320px of the window). The width
28
+ // is the token --nk-peek-width, set on :root; `width` (px) sets it too, and
29
+ // nk-resize { width } fires when a drag or a key ends – the moment to keep
30
+ // it. resize-label names the edge (default "Resize").
31
+ // `inset`: while it is open on the desktop, the page's <nk-app> gets
32
+ // `peek-inset` – its main column makes room instead of lying under the peek.
24
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>';
25
- const OVERLAYS = new Set(['nk-menu', 'nk-pop', 'nk-modal', 'nk-sheet', 'nk-cmdk', 'nk-toast']);
26
- const OVERLAY_CLASSES = ['nk-pop', 'nk-modal-backdrop', 'nk-sheet-backdrop', 'nk-cmdk-backdrop'];
34
+ const OVERLAYS = new Set(['nk-menu', 'nk-pop', 'nk-modal', 'nk-sheet', 'nk-dialog', 'nk-cmdk', 'nk-toast']);
35
+ const OVERLAY_CLASSES = ['nk-pop', 'nk-modal-backdrop', 'nk-sheet-backdrop', 'nk-dialog-backdrop', 'nk-cmdk-backdrop'];
27
36
  const phone = () => matchMedia('(max-width: 860px)').matches;
28
37
 
29
38
  class NkPeek extends NkElement {
30
- static get observedAttributes() { return ['open', 'label', 'close-label']; }
39
+ static get observedAttributes() { return ['open', 'label', 'close-label', 'resizable', 'width', 'min', 'max', 'resize-label', 'inset']; }
31
40
 
32
41
  render() {
33
42
  this._backdrop = this.createElement('div', ['nk-peek-backdrop']);
34
43
  this._box = this.createElement('aside', ['nk-peek'], { role: 'dialog', tabindex: '-1' });
44
+ this._handle = this.createElement('div', ['pk-resize'], { role: 'separator', 'aria-orientation': 'vertical', tabindex: '0' });
45
+ this._box.appendChild(this._handle);
35
46
  const bar = this.createElement('div', ['pk-bar']);
36
47
  this._close = this.createElement('button', ['nk-topbar-btn'], { type: 'button' });
37
48
  this._close.innerHTML = CLOSE_ICON;
@@ -42,9 +53,41 @@ class NkPeek extends NkElement {
42
53
  this._backdrop.appendChild(this._box);
43
54
  this._wrapper.appendChild(this._backdrop);
44
55
  this._syncLabels();
56
+ this._syncResize();
57
+ if (this.hasAttribute('width')) this._setWidth(Number(this.getAttribute('width')));
45
58
  this._syncOpen();
46
59
  }
47
60
 
61
+ // ── width ──
62
+ get _min() { return Number(this.getAttribute('min')) || 380; }
63
+ get _max() { return Number(this.getAttribute('max')) || Math.max(this._min, innerWidth - 320); }
64
+ /** The peek's width in px – the token --nk-peek-width. */
65
+ get width() { return parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nk-peek-width')) || 560; }
66
+ set width(v) { this._setWidth(Number(v)); }
67
+
68
+ _setWidth(w) {
69
+ if (!Number.isFinite(w)) return;
70
+ w = Math.round(Math.min(Math.max(w, this._min), Math.max(this._min, this._max)));
71
+ document.documentElement.style.setProperty('--nk-peek-width', `${w}px`);
72
+ this._handle.setAttribute('aria-valuenow', w);
73
+ }
74
+
75
+ _syncResize() {
76
+ this._handle.hidden = !this.getBoolAttr('resizable');
77
+ this._handle.setAttribute('aria-label', this.getAttribute('resize-label') || 'Resize');
78
+ this._handle.setAttribute('aria-valuemin', this._min);
79
+ this._handle.setAttribute('aria-valuenow', Math.round(this.width));
80
+ }
81
+
82
+ /** The app whose main column makes room: the peek's own, else the page's. */
83
+ _app() { return this.closest('nk-app') ?? document.querySelector('nk-app'); }
84
+ _syncInset() {
85
+ const on = this.getBoolAttr('inset') && this.getBoolAttr('open') && !phone();
86
+ if (on) this._app()?.setAttribute('peek-inset', '');
87
+ else if (this._insetApp) this._app()?.removeAttribute('peek-inset');
88
+ this._insetApp = on;
89
+ }
90
+
48
91
  _syncLabels() {
49
92
  const label = this.getAttribute('label');
50
93
  if (label) this._box.setAttribute('aria-label', label); else this._box.removeAttribute('aria-label');
@@ -54,6 +97,7 @@ class NkPeek extends NkElement {
54
97
  _syncOpen() {
55
98
  const open = this.getBoolAttr('open');
56
99
  this._backdrop.classList.toggle('open', open);
100
+ this._syncInset();
57
101
  if (open === this._wasOpen) return;
58
102
  this._wasOpen = open;
59
103
  if (open) {
@@ -88,23 +132,54 @@ class NkPeek extends NkElement {
88
132
  if (path.some(n => n.nodeType === 1 && (OVERLAYS.has(n.localName) || OVERLAY_CLASSES.some(c => n.classList.contains(c))))) return;
89
133
  this.close();
90
134
  };
135
+ // The left edge: a drag with pointer capture, or the arrow keys.
136
+ const resized = () => this.emit('nk-resize', { width: Math.round(this.width) });
137
+ this._onResizeStart = (e) => {
138
+ if (e.button !== 0) return;
139
+ e.preventDefault();
140
+ this._handle.setPointerCapture(e.pointerId);
141
+ this._handle.classList.add('active');
142
+ };
143
+ this._onResizeMove = (e) => { if (this._handle.hasPointerCapture(e.pointerId)) this._setWidth(innerWidth - e.clientX); };
144
+ this._onResizeEnd = () => { this._handle.classList.remove('active'); resized(); };
145
+ this._onResizeKey = (e) => {
146
+ const step = { ArrowLeft: 16, ArrowRight: -16 }[e.key];
147
+ if (!step) return;
148
+ e.preventDefault();
149
+ this._setWidth(this.width + step);
150
+ resized();
151
+ };
152
+ this._onViewport = () => this._syncInset();
91
153
  this._close.addEventListener('click', this._onClose);
92
154
  this._backdrop.addEventListener('click', this._onBackdrop);
155
+ this._handle.addEventListener('pointerdown', this._onResizeStart);
156
+ this._handle.addEventListener('pointermove', this._onResizeMove);
157
+ this._handle.addEventListener('lostpointercapture', this._onResizeEnd);
158
+ this._handle.addEventListener('keydown', this._onResizeKey);
93
159
  document.addEventListener('keydown', this._onKey);
94
160
  document.addEventListener('click', this._onOutside);
161
+ addEventListener('resize', this._onViewport);
95
162
  }
96
163
 
97
164
  teardownEvents() {
98
165
  this._close?.removeEventListener('click', this._onClose);
99
166
  this._backdrop?.removeEventListener('click', this._onBackdrop);
167
+ this._handle?.removeEventListener('pointerdown', this._onResizeStart);
168
+ this._handle?.removeEventListener('pointermove', this._onResizeMove);
169
+ this._handle?.removeEventListener('lostpointercapture', this._onResizeEnd);
170
+ this._handle?.removeEventListener('keydown', this._onResizeKey);
100
171
  document.removeEventListener('keydown', this._onKey);
101
172
  document.removeEventListener('click', this._onOutside);
173
+ removeEventListener('resize', this._onViewport);
174
+ if (this._insetApp) { this._app()?.removeAttribute('peek-inset'); this._insetApp = false; }
102
175
  if (this._modal) { this._modal = false; unlockScroll(); this._undoInert?.(); this._undoInert = null; }
103
176
  this._wasOpen = false;
104
177
  }
105
178
 
106
- onAttributeChanged(name) {
107
- if (name !== 'open') { this._syncLabels(); return; }
179
+ onAttributeChanged(name, _old, value) {
180
+ if (name === 'width') { this._setWidth(Number(value)); return; }
181
+ if (name === 'inset') { this._syncInset(); return; }
182
+ if (name !== 'open') { this._syncLabels(); this._syncResize(); return; }
108
183
  this._syncOpen();
109
184
  this.emit('nk-toggle', { open: this.getBoolAttr('open') });
110
185
  }
@@ -1,4 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
+ import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
2
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
3
4
 
4
5
  // <nk-sidebar slot="sidebar">
@@ -16,13 +17,28 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
16
17
  // .open) – the sidebar slides and the scrim fades by the same rules as the
17
18
  // class markup; the element toggles the two classes. <nk-btn variant="sidebar">
18
19
  // in the topbar is the ☰ that opens it.
20
+ //
21
+ // `collapsible` (NotionKit 1.10.0): on the desktop Notion's « beside the
22
+ // workspace row collapses the sidebar – it slides out to the left, the main
23
+ // column takes the width, nothing in it takes focus – and the topbar's ☰
24
+ // brings it back; ⌘\ or Ctrl+\ toggles it (on a phone the drawer).
25
+ // `collapsed` is the state, collapse() / expand() / toggleCollapsed() the
26
+ // API, nk-collapse { collapsed } the moment to keep it. collapse-label names
27
+ // the « (default "Close sidebar") and is its tooltip for <nk-tooltip>.
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>';
29
+ const phone = () => matchMedia('(max-width: 860px)').matches;
30
+
19
31
  class NkSidebar extends NkElement {
20
- static get observedAttributes() { return ['open']; }
32
+ static get observedAttributes() { return ['open', 'collapsible', 'collapsed', 'collapse-label']; }
21
33
 
22
34
  render() {
23
35
  this._backdrop = this.createElement('div', ['nk-sidebar-backdrop']);
24
36
  this._aside = this.createElement('aside', ['nk-sidebar']);
25
- this._aside.appendChild(this.createElement('slot', [], { name: 'workspace' }));
37
+ const head = this.createElement('div', ['nk-sidebar-head']);
38
+ this._collapse = this.createElement('button', ['nk-sidebar-collapse'], { type: 'button', 'data-tooltip-key': '⌘\\' });
39
+ this._collapse.innerHTML = COLLAPSE_ICON;
40
+ head.append(this.createElement('slot', [], { name: 'workspace' }), this._collapse);
41
+ this._aside.appendChild(head);
26
42
  const scroll = this.createElement('div', ['nk-sidebar-scroll']);
27
43
  scroll.appendChild(document.createElement('slot'));
28
44
  this._aside.appendChild(scroll);
@@ -34,6 +50,21 @@ class NkSidebar extends NkElement {
34
50
  this._wrapper.appendChild(this._aside);
35
51
  this._syncFooter();
36
52
  this._syncOpen();
53
+ this._syncCollapse();
54
+ }
55
+
56
+ _syncCollapse() {
57
+ const label = this.getAttribute('collapse-label') || 'Close sidebar';
58
+ this._collapse.hidden = !this.getBoolAttr('collapsible');
59
+ this._collapse.setAttribute('aria-label', label);
60
+ this._collapse.dataset.tooltip = label;
61
+ this._aside.classList.toggle('collapsed', this.getBoolAttr('collapsed'));
62
+ }
63
+
64
+ /** The ☰ that brings the sidebar back: the one in its own app, else the page's. */
65
+ _toggleButton() {
66
+ const sel = 'nk-btn[variant="sidebar"]';
67
+ return this.closest('nk-app')?.querySelector(sel) ?? document.querySelector(sel);
37
68
  }
38
69
 
39
70
  _syncOpen() {
@@ -53,22 +84,47 @@ class NkSidebar extends NkElement {
53
84
  setupEvents() {
54
85
  this._onSlot = () => this._syncFooter();
55
86
  this._onBackdrop = () => this.close();
56
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
87
+ this._onKey = (e) => {
88
+ if (e.key === 'Escape' && this.getBoolAttr('open')) this.close();
89
+ // Notion's ⌘\: the collapse on the desktop, the drawer on a phone.
90
+ if (e.key === '\\' && (e.metaKey || e.ctrlKey) && this.getBoolAttr('collapsible') && !e.defaultPrevented) {
91
+ e.preventDefault();
92
+ phone() ? this.toggle() : this.toggleCollapsed();
93
+ }
94
+ };
95
+ this._onCollapse = () => this.collapse();
57
96
  this._footerSlot.addEventListener('slotchange', this._onSlot);
58
97
  this._backdrop.addEventListener('click', this._onBackdrop);
98
+ this._collapse.addEventListener('click', this._onCollapse);
59
99
  document.addEventListener('keydown', this._onKey);
60
100
  }
61
101
 
62
102
  teardownEvents() {
63
103
  this._footerSlot?.removeEventListener('slotchange', this._onSlot);
64
104
  this._backdrop?.removeEventListener('click', this._onBackdrop);
105
+ this._collapse?.removeEventListener('click', this._onCollapse);
65
106
  document.removeEventListener('keydown', this._onKey);
66
107
  }
67
108
 
68
109
  onAttributeChanged(name) {
69
- if (name !== 'open') return;
70
- this._syncOpen();
71
- this.emit('nk-toggle', { open: this.getBoolAttr('open') });
110
+ if (name === 'open') {
111
+ this._syncOpen();
112
+ this.emit('nk-toggle', { open: this.getBoolAttr('open') });
113
+ return;
114
+ }
115
+ // Focus follows to the control that is left: the ☰ after collapsing, the « after expanding.
116
+ const active = deepActiveElement();
117
+ const fromSidebar = active && (this.contains(active) || this._shadow.contains(active));
118
+ const fromToggle = active?.closest?.('.nk-sidebar-toggle');
119
+ this._syncCollapse();
120
+ if (name !== 'collapsed') return;
121
+ const collapsed = this.getBoolAttr('collapsed');
122
+ this.emit('nk-collapse', { collapsed });
123
+ // Two frames later: with reduced motion every nk- element transitions for
124
+ // .01ms, the inherited visibility included, so the « is focusable only then.
125
+ const later = fn => requestAnimationFrame(() => requestAnimationFrame(fn));
126
+ if (collapsed && fromSidebar) later(() => this._toggleButton()?.focus({ preventScroll: true }));
127
+ if (!collapsed && fromToggle && !this._collapse.hidden) later(() => this._collapse.focus({ preventScroll: true }));
72
128
  }
73
129
 
74
130
  show() { this.setBoolAttr('open', true); }
@@ -77,6 +133,12 @@ class NkSidebar extends NkElement {
77
133
 
78
134
  get open() { return this.getBoolAttr('open'); }
79
135
  set open(v) { this.setBoolAttr('open', v); }
136
+
137
+ collapse() { this.setBoolAttr('collapsed', true); }
138
+ expand() { this.setBoolAttr('collapsed', false); }
139
+ toggleCollapsed() { this.setBoolAttr('collapsed', !this.getBoolAttr('collapsed')); }
140
+ get collapsed() { return this.getBoolAttr('collapsed'); }
141
+ set collapsed(v) { this.setBoolAttr('collapsed', v); }
80
142
  }
81
143
 
82
144
  customElements.define('nk-sidebar', NkSidebar);
@@ -7,8 +7,10 @@ import './shared/avatar-BiqCaHOt.js';
7
7
  // `wrap` lets cell text break (`.nk-table.wrap`), like Notion's "wrap column".
8
8
  // Standalone: view.columns = […]; view.rows = […]; Inside <nk-database> the
9
9
  // database pushes `data`. → <div class="nk-table-wrap"><table class="nk-table">…</table><div class="nk-new-row">+ New page</div></div>
10
- // Cells are plain markup from renderPropertyCell(); rows fire nk-select,
11
- // checkboxes nk-change, headers nk-action { action: 'sort' } (and sort locally
10
+ // Cells are plain markup from renderPropertyCell(); rows fire nk-select
11
+ // { row, id, key, cell } – `key` and `cell` say which cell was hit, to open
12
+ // an editor there, like the date picker under a due date – checkboxes
13
+ // nk-change, headers nk-action { action: 'sort' } (and sort locally
12
14
  // with `sortable`), the add row nk-action { action: 'new-row' }.
13
15
  class NkTableView extends NkElement {
14
16
  static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
@@ -76,7 +78,11 @@ class NkTableView extends NkElement {
76
78
  }
77
79
  if (e.target.closest('input[type=checkbox], a')) return;
78
80
  const tr = e.target.closest('tr[data-id]');
79
- if (tr) { const row = this._rowById(tr.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
81
+ if (!tr) return;
82
+ // Which cell: `key` names its column, `cell` is there to anchor an editor to.
83
+ const cell = e.target.closest('td');
84
+ const row = this._rowById(tr.dataset.id);
85
+ this.emit('nk-select', { row, id: row?.id, value: row?.id, key: cell ? this._columns[cell.cellIndex]?.key : undefined, cell });
80
86
  };
81
87
  this._onChange = (e) => {
82
88
  const input = e.target;
@@ -0,0 +1,138 @@
1
+ import { NkElement } from './base.js';
2
+ import { a as placeNear } from './shared/floating-CxHxy-Cb.js';
3
+ import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
4
+ import '@jungherz-de/notionkit/notionkit-styles.js';
5
+
6
+ // <nk-tooltip></nk-tooltip> one for the page: every [data-tooltip]
7
+ // <button data-tooltip="Close sidebar" data-tooltip-key="⌘\">«</button>
8
+ // <nk-tooltip for="saveBtn" shortcut="⌘S">Save the page</nk-tooltip>
9
+ // tip.show(barOrRect, 'Shell · 12.10.–30.11.'); tip.hide();
10
+ // → <div class="nk-tooltip open" role="tooltip">Close sidebar<span class="tt-key">⌘\</span></div>
11
+ //
12
+ // Notion's hover hint (NotionKit 1.10.0). Without `for` it serves every
13
+ // element with data-tooltip – and data-tooltip-key for a shortcut – on the
14
+ // page, inside shadow roots too: the tooltips of a whole toolbar in one
15
+ // element. With `for` it belongs to the element with that id and shows its
16
+ // own content and `shortcut`. It appears after `delay` ms (400) under the
17
+ // pointer and at once on keyboard focus, never on touch; leaving, a press,
18
+ // blur, scrolling and Escape hide it. It sits 6px below its target and
19
+ // centred, above it where the window ends – placement="top" prefers above –
20
+ // and 8px inside the window (placeNear in util/floating.js). show(target,
21
+ // text, shortcut) takes an element or a rect, for what has no element of
22
+ // its own: the bars of a Gantt chart. While it shows, the target's
23
+ // aria-describedby names it (in the same document or shadow root).
24
+ const hostSheet = new CSSStyleSheet();
25
+ hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
26
+ let count = 0;
27
+
28
+ class NkTooltip extends NkElement {
29
+ static get hostStyles() { return hostSheet; }
30
+ static get observedAttributes() { return ['for', 'shortcut']; }
31
+
32
+ render() {
33
+ this._bubble = this.createElement('div', ['nk-tooltip'], { role: 'tooltip' });
34
+ this._slot = document.createElement('slot');
35
+ this._text = document.createTextNode('');
36
+ this._key = this.createElement('span', ['tt-key']);
37
+ this._bubble.append(this._slot, this._text, this._key);
38
+ this._wrapper.appendChild(this._bubble);
39
+ this._fill(null);
40
+ }
41
+
42
+ get _delay() { const d = Number(this.getAttribute('delay')); return this.hasAttribute('delay') && d >= 0 ? d : 400; }
43
+
44
+ /** Own content (text null) or a given text and shortcut. */
45
+ _fill(text, key = this.getAttribute('shortcut')) {
46
+ this._slot.style.display = text == null ? '' : 'none';
47
+ this._text.data = text ?? '';
48
+ this._key.textContent = key || '';
49
+ this._key.hidden = !key;
50
+ }
51
+
52
+ /** Shows it at `target` – an element or a rect – with `text` (else its own content) and `shortcut`. */
53
+ show(target, text, shortcut) {
54
+ clearTimeout(this._timer);
55
+ if (!target) return;
56
+ this._fill(text ?? null, shortcut);
57
+ this._describe(target);
58
+ placeNear(this, target, this.getAttribute('placement') === 'top' ? 'top' : 'bottom');
59
+ this._bubble.classList.add('open');
60
+ }
61
+
62
+ hide() {
63
+ clearTimeout(this._timer);
64
+ this._bubble?.classList.remove('open');
65
+ this._describe(null);
66
+ this._target = null;
67
+ }
68
+
69
+ get open() { return !!this._bubble?.classList.contains('open'); }
70
+
71
+ _describe(target) {
72
+ if (this._described) {
73
+ const ids = (this._described.getAttribute('aria-describedby') || '').split(/\s+/).filter(id => id && id !== this.id);
74
+ ids.length ? this._described.setAttribute('aria-describedby', ids.join(' ')) : this._described.removeAttribute('aria-describedby');
75
+ this._described = null;
76
+ }
77
+ if (!(target instanceof Element) || target.getRootNode() !== this.getRootNode()) return;
78
+ this.id ||= `nk-tooltip-${++count}`;
79
+ const ids = (target.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
80
+ if (!ids.includes(this.id)) target.setAttribute('aria-describedby', [...ids, this.id].join(' '));
81
+ this._described = target;
82
+ }
83
+
84
+ /** The element this tooltip serves for an event: its `for` target, or the nearest [data-tooltip]. */
85
+ _targetOf(e) {
86
+ const path = e.composedPath();
87
+ const own = this.getAttribute('for');
88
+ if (own) { const el = this.getRootNode().getElementById?.(own); return el && path.includes(el) ? el : null; }
89
+ return path.find(n => n instanceof Element && n.hasAttribute('data-tooltip')) ?? null;
90
+ }
91
+
92
+ _showFor(el) {
93
+ const own = this.hasAttribute('for');
94
+ this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
95
+ }
96
+
97
+ setupEvents() {
98
+ this._onOver = (e) => {
99
+ const el = e.pointerType === 'touch' ? null : this._targetOf(e);
100
+ if (el === this._target) return;
101
+ this.hide();
102
+ if (!el) return;
103
+ this._target = el;
104
+ this._timer = setTimeout(() => this._showFor(el), this._delay);
105
+ };
106
+ this._onFocus = (e) => {
107
+ const el = this._targetOf(e);
108
+ if (!el || !deepActiveElement()?.matches(':focus-visible')) return;
109
+ this.hide();
110
+ this._target = el;
111
+ this._showFor(el);
112
+ };
113
+ this._onHide = () => this.hide();
114
+ this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
115
+ document.addEventListener('pointerover', this._onOver);
116
+ document.addEventListener('focusin', this._onFocus);
117
+ document.addEventListener('focusout', this._onHide);
118
+ document.addEventListener('pointerdown', this._onHide, true);
119
+ document.addEventListener('scroll', this._onHide, true);
120
+ document.addEventListener('keydown', this._onKey);
121
+ }
122
+
123
+ teardownEvents() {
124
+ document.removeEventListener('pointerover', this._onOver);
125
+ document.removeEventListener('focusin', this._onFocus);
126
+ document.removeEventListener('focusout', this._onHide);
127
+ document.removeEventListener('pointerdown', this._onHide, true);
128
+ document.removeEventListener('scroll', this._onHide, true);
129
+ document.removeEventListener('keydown', this._onKey);
130
+ this.hide();
131
+ }
132
+
133
+ onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }
134
+ }
135
+
136
+ customElements.define('nk-tooltip', NkTooltip);
137
+
138
+ export { NkTooltip };
@@ -9,7 +9,7 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
9
9
  // row; clicking outside or pressing Escape closes it.
10
10
  const menuSheet = new CSSStyleSheet();
11
11
  menuSheet.replaceSync(`
12
- .nk-ws-anchor { position: relative; }
12
+ .nk-ws-anchor { position: relative; flex: 1 1 auto; min-width: 0; }
13
13
  .nk-ws-menu { display: none; position: absolute; left: 8px; top: calc(100% - 2px); z-index: 50; }
14
14
  :host([open]) .nk-ws-menu { display: block; }
15
15
  `);
@@ -0,0 +1,67 @@
1
+ // Calendar days for nk-calendar and nk-calendar-view: local days as
2
+ // 'YYYY-MM-DD', no library. Weeks are ISO weeks (Monday-based, the week with
3
+ // the year's first Thursday); the first weekday comes from the locale.
4
+ const pad = n => String(n).padStart(2, '0');
5
+
6
+ const isoOf = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
7
+ const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
8
+
9
+ /** 'YYYY-MM-DD[THH:MM]', 'D.M.YYYY', a Date or { start } → a local Date at midnight, or null. */
10
+ function parseDay(v) {
11
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : new Date(v.getFullYear(), v.getMonth(), v.getDate());
12
+ if (v && typeof v === 'object') return v.start ? parseDay(v.start) : null;
13
+ const s = String(v ?? '').trim();
14
+ let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
15
+ if (m) return new Date(+m[1], +m[2] - 1, +m[3]);
16
+ m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
17
+ return m ? new Date(+m[3], +m[2] - 1, +m[1]) : null;
18
+ }
19
+
20
+ function isoWeek(d) {
21
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
22
+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
23
+ return Math.ceil(((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5 + 1) / 7);
24
+ }
25
+
26
+ /** The page's language unless the element names one. */
27
+ const resolveLocale = attr => attr || document.documentElement.lang || navigator.language || 'en';
28
+
29
+ /** 0 = Sunday … 6 = Saturday, from Intl.Locale's week info; Monday where the browser cannot say. */
30
+ function firstWeekday(locale) {
31
+ try {
32
+ const l = new Intl.Locale(locale), info = l.getWeekInfo?.() ?? l.weekInfo;
33
+ if (info?.firstDay) return info.firstDay % 7;
34
+ } catch { /* an unknown tag */ }
35
+ return 1;
36
+ }
37
+
38
+ /** Short weekday names from `weekStart` on; `length` cuts them (2 → "Mo"). */
39
+ function weekdayNames(locale, weekStart, length = 0) {
40
+ const f = new Intl.DateTimeFormat(locale, { weekday: 'short' });
41
+ // 7 June 2026 is a Sunday, so 7 + n is weekday n.
42
+ return Array.from({ length: 7 }, (_, i) => {
43
+ const name = f.format(new Date(2026, 5, 7 + ((weekStart + i) % 7))).replace('.', '');
44
+ return length ? name.slice(0, length) : name;
45
+ });
46
+ }
47
+
48
+ /** A month from its week start: `first`, `day(i)` for the i-th cell, the `rows` it needs. */
49
+ function monthGrid(month, weekStart) {
50
+ const [y, m] = month.split('-').map(Number), first = new Date(y, m - 1, 1), offset = (first.getDay() - weekStart + 7) % 7;
51
+ return { m, first, day: i => new Date(y, m - 1, 1 - offset + i), rows: Math.ceil((offset + new Date(y, m, 0).getDate()) / 7) };
52
+ }
53
+
54
+ function stepMonth(month, n) {
55
+ const [y, m] = month.split('-').map(Number);
56
+ return isoOf(new Date(y, m - 1 + n, 1)).slice(0, 7);
57
+ }
58
+
59
+ /** "KW" for German, "W" elsewhere – the head of the week column. */
60
+ const weekLabel = locale => (/^de\b/i.test(locale) ? 'KW' : 'W');
61
+
62
+ const CHEVRONS = {
63
+ prev: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>',
64
+ next: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>',
65
+ };
66
+
67
+ export { CHEVRONS as C, weekdayNames as a, isoWeek as b, addDays as c, firstWeekday as f, isoOf as i, monthGrid as m, parseDay as p, resolveLocale as r, stepMonth as s, weekLabel as w };
@@ -0,0 +1,37 @@
1
+ // Placement for elements that float over the page – <nk-menu floating>,
2
+ // <nk-calendar floating>: the host is a fixed box put under its anchor.
3
+
4
+ /** The border box of an element, or of the first descendant with one (display: contents hosts). */
5
+ function boxOf(el) {
6
+ if (el.getClientRects().length) return el.getBoundingClientRect();
7
+ for (const child of [...(el.shadowRoot?.children ?? []), ...el.children]) {
8
+ const r = boxOf(child);
9
+ if (r.width || r.height) return r;
10
+ }
11
+ return el.getBoundingClientRect();
12
+ }
13
+
14
+ /**
15
+ * Puts `host` 6px below `target` – an element or a rect ({ left, top, right,
16
+ * bottom }, a DOMRect) – centred on it: above with side="top", or where the
17
+ * window ends below; kept 8px inside the window. For <nk-tooltip>.
18
+ */
19
+ function placeNear(host, target, side = 'bottom', gap = 6) {
20
+ const r = target instanceof Element ? boxOf(target) : target;
21
+ const { width: w, height: h } = host.getBoundingClientRect();
22
+ const below = r.bottom + gap, above = r.top - gap - h;
23
+ const top = side === 'top' ? (above < 8 ? below : above) : (below + h > innerHeight - 8 ? above : below);
24
+ host.style.top = `${Math.round(top)}px`;
25
+ host.style.left = `${Math.round(Math.min(Math.max(8, (r.left + r.right) / 2 - w / 2), innerWidth - 8 - w))}px`;
26
+ host.style.right = '';
27
+ }
28
+
29
+ /** Puts `host` 6px under `anchor`, right edges aligned – left edges with align="start". */
30
+ function placeUnder(host, anchor, align) {
31
+ const r = boxOf(anchor);
32
+ host.style.top = `${r.bottom + 6}px`;
33
+ if (align === 'start') { host.style.left = `${r.left}px`; host.style.right = ''; }
34
+ else { host.style.right = `${Math.max(8, innerWidth - r.right)}px`; host.style.left = ''; }
35
+ }
36
+
37
+ export { placeNear as a, placeUnder as p };