@jungherz-de/notionkit-elements 1.10.0 → 1.11.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 (41) hide show
  1. package/README.md +6 -6
  2. package/dist/components/nk-ai-msg.js +6 -4
  3. package/dist/components/nk-avatar.js +1 -1
  4. package/dist/components/nk-banner.js +5 -2
  5. package/dist/components/nk-board-view.js +2 -2
  6. package/dist/components/nk-calendar.js +2 -2
  7. package/dist/components/nk-comment.js +1 -1
  8. package/dist/components/nk-dialog.js +14 -8
  9. package/dist/components/nk-fields.js +13 -3
  10. package/dist/components/nk-filter-bar.js +5 -3
  11. package/dist/components/nk-list-view.js +2 -2
  12. package/dist/components/nk-member-row.js +1 -1
  13. package/dist/components/nk-menu.js +2 -2
  14. package/dist/components/nk-page.js +3 -2
  15. package/dist/components/nk-select.js +15 -7
  16. package/dist/components/nk-switch.js +5 -1
  17. package/dist/components/nk-table-view.js +2 -2
  18. package/dist/components/nk-tooltip.js +17 -7
  19. package/dist/components/nk-tree-item.js +10 -4
  20. package/dist/components/shared/{avatar-BiqCaHOt.js → avatar-CzPZN3uP.js} +2 -2
  21. package/dist/components/shared/floating-CtqV6MBj.js +66 -0
  22. package/dist/components/shared/{property-cell-o7N30kAQ.js → property-cell-Cddz5n2O.js} +1 -1
  23. package/dist/notionkit-elements.esm.js +137 -56
  24. package/dist/notionkit-elements.js +137 -56
  25. package/dist/notionkit-elements.min.js +1 -1
  26. package/package.json +3 -3
  27. package/src/components/data/nk-ai-msg.js +6 -4
  28. package/src/components/data/nk-filter-bar.js +5 -3
  29. package/src/components/forms/nk-calendar.js +1 -1
  30. package/src/components/forms/nk-fields.js +13 -3
  31. package/src/components/forms/nk-select.js +15 -7
  32. package/src/components/forms/nk-switch.js +5 -1
  33. package/src/components/overlays/nk-dialog.js +14 -8
  34. package/src/components/overlays/nk-menu.js +1 -1
  35. package/src/components/overlays/nk-tooltip.js +16 -6
  36. package/src/components/page/nk-banner.js +5 -2
  37. package/src/components/page/nk-page.js +3 -2
  38. package/src/components/shell/nk-tree-item.js +10 -4
  39. package/src/util/avatar.js +2 -2
  40. package/src/util/floating.js +39 -10
  41. package/dist/components/shared/floating-CxHxy-Cb.js +0 -37
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <h1 align="center">🧩 NotionKit Elements</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.10.0-2383e2?style=flat-square" alt="Version"></a>
4
+ <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.11.0-2383e2?style=flat-square" alt="Version"></a>
5
5
  <a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-448361?style=flat-square" alt="Vanilla JS"></a>
6
6
  <a href="#"><img src="https://img.shields.io/badge/elements-87-529cca?style=flat-square" alt="87 Elements"></a>
7
7
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-9065b0?style=flat-square" alt="MIT License"></a>
8
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.10.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.11.0-lightgrey?style=flat-square" alt="Changelog"></a>
9
9
  <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fnotionkit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
10
10
  </p>
11
11
 
@@ -59,7 +59,7 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
59
59
  | 🧩 **87 Elements** | Forms with copy field, image picker and date picker, content blocks with bookmarks, app shell & tree with the ☰ drawer and the collapsing sidebar, page shell with properties, panels, avatars and steps, overlays (modal, sheet, dialog, resizable side peek, tooltip, ⌘K palette, menus that float in and become sheets on a phone, popover, emoji picker, toast), database table, board, list & calendar with toolbar and filter pills, comments, AI thread |
60
60
  | 🎛️ **Form participation** | Input, textarea, select, switch, check, radio, slider, segmented and model card work natively with `<form>` via `ElementInternals`; radios group across shadow roots |
61
61
  | 🌗 **Theme sync** | One observer mirrors `data-theme` on `<html>` into every element |
62
- | 🪶 **Lightweight** | 265 KB minified / 52 KB gzipped (IIFE, foundation sheet included), no external dependencies |
62
+ | 🪶 **Lightweight** | 267 KB minified / 53 KB gzipped (IIFE, foundation sheet included), no external dependencies |
63
63
  | 📦 **Four bundle shapes** | IIFE, minified IIFE, ESM, and per-component ESM entries on a stable `base.js`, which import NotionKit's sheet instead of carrying a copy |
64
64
  | 🤖 **AI-ready** | `SKILL.md` with copy-paste markup, six app skeletons and the rules – an agent given only that file built a working app in the release test |
65
65
 
@@ -71,8 +71,8 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
71
71
  <!DOCTYPE html>
72
72
  <html lang="en" data-theme="light">
73
73
  <head>
74
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.10.0/notionkit.min.css">
75
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.10.0/dist/notionkit-elements.min.js"></script>
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.11.0/notionkit.min.css">
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.11.0/dist/notionkit-elements.min.js"></script>
76
76
  </head>
77
77
  <body class="nk-body">
78
78
  <nk-field label="Display name" desc="Shown next to your comments">
@@ -102,7 +102,7 @@ The per-component files import NotionKit's sheet as `@jungherz-de/notionkit/noti
102
102
 
103
103
  Do not mix the full bundle with the per-component files: each brings its own `NkElement`. With the bundle, take `componentsSheet` and `NkElement` from the bundle (`NotionKitElements.componentsSheet` from the `<script>` build).
104
104
 
105
- Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.10.0`; 1.10.0 brings the collapsing sidebar, the resizable side peek, the dialog and the tooltip, 1.9.0 the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
105
+ Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.11.0`; 1.11.0 keeps a floating menu inside an open modal or dialog in place and inside the window and brings the banner's action as a button, fields that share a row and tooltips of several lines, 1.10.1 lets a disabled button show its tooltip, 1.10.0 brings the collapsing sidebar, the resizable side peek, the dialog and the tooltip, 1.9.0 the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
106
106
 
107
107
  ## Elements
108
108
 
@@ -38,13 +38,15 @@ class NkAiMsg extends NkElement {
38
38
  this._box.classList.toggle('user', user);
39
39
  this._box.classList.toggle('bubble', bubble);
40
40
  const name = this.getAttribute('name');
41
- this._nameText.data = name || '';
42
- const badge = this.getAttribute('badge');
43
- this._badge.textContent = badge ? ` ${badge}` : '';
41
+ // Runs on slotchange: write text on a change only (see nk-switch).
42
+ if (this._nameText.data !== (name || '')) this._nameText.data = name || '';
43
+ const badge = this.getAttribute('badge'), badgeText = badge ? ` ${badge}` : '';
44
+ if (this._badge.textContent !== badgeText) this._badge.textContent = badgeText;
44
45
  // A bubble stands without avatar and name; the text says who asked.
45
46
  this._name.style.display = name && !bubble ? '' : 'none';
46
47
  this._avatarSlot.style.display = bubble ? 'none' : '';
47
- this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
48
+ const avatar = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
49
+ if (this._avatar.textContent !== avatar) this._avatar.textContent = avatar;
48
50
  const color = this.getAttribute('color');
49
51
  this._avatar.style.background = color || '';
50
52
  this._actions.style.display = this._actionSlot.assignedElements().length ? '' : 'none';
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { p as paintAvatar, i as initialsOf } from './shared/avatar-BiqCaHOt.js';
2
+ import { p as paintAvatar, i as initialsOf } from './shared/avatar-CzPZN3uP.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
 
5
5
  // <nk-avatar>MK</nk-avatar> → <span class="nk-avatar">MK</span>
@@ -1,8 +1,11 @@
1
1
  import { NkElement } from './base.js';
2
2
  import '@jungherz-de/notionkit/notionkit-styles.js';
3
3
 
4
- // <nk-banner variant="info">ℹ️ <span>Text</span><span slot="action">Open</span></nk-banner>
5
- // → <div class="nk-banner info">…<span class="b-action">Open</span></div>
4
+ // <nk-banner variant="info">ℹ️ <span>Text</span><button slot="action" type="button">Open</button></nk-banner>
5
+ // → <div class="nk-banner info">…<button class="b-action" type="button">Open</button></div>
6
+ //
7
+ // The action is a <button>, which NotionKit (1.11.0) strips down to
8
+ // underlined text; below 860px it moves under the text.
6
9
  const VARIANTS = ['info', 'success', 'warning', 'danger'];
7
10
 
8
11
  class NkBanner extends NkElement {
@@ -1,7 +1,7 @@
1
1
  import { NkElement } from './base.js';
2
- import { t as tagFor, r as renderPropertyCell } from './shared/property-cell-o7N30kAQ.js';
2
+ import { t as tagFor, r as renderPropertyCell } from './shared/property-cell-Cddz5n2O.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
- import './shared/avatar-BiqCaHOt.js';
4
+ import './shared/avatar-CzPZN3uP.js';
5
5
 
6
6
  // <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
7
7
  // Groups rows by a select column (default: the first select column); one
@@ -1,6 +1,6 @@
1
1
  import { NkFormElement } from './base.js';
2
2
  import { p as parseDay, i as isoOf, C as CHEVRONS, r as resolveLocale, f as firstWeekday, m as monthGrid, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth, c as addDays } from './shared/dates-a32DcW1l.js';
3
- import { p as placeUnder } from './shared/floating-CxHxy-Cb.js';
3
+ import { p as placeUnder } from './shared/floating-CtqV6MBj.js';
4
4
  import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
5
5
  import '@jungherz-de/notionkit/notionkit-styles.js';
6
6
 
@@ -44,7 +44,7 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
44
44
  // outside does. Put a floating calendar directly under <body>.
45
45
  const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
46
46
  const floatSheet = new CSSStyleSheet();
47
- floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
47
+ floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none; }`);
48
48
 
49
49
  class NkCalendar extends NkFormElement {
50
50
  static get hostStyles() { return floatSheet; }
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { i as initialsOf, p as paintAvatar } from './shared/avatar-BiqCaHOt.js';
2
+ import { i as initialsOf, p as paintAvatar } from './shared/avatar-CzPZN3uP.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
 
5
5
  // <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="green">The board view feels close.</nk-comment>
@@ -18,10 +18,13 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
18
18
  // scroll-locked. It lies above the modal and the sheet, so a question can
19
19
  // come from either. 440px, `wide` 560px; below 860px a bottom sheet with the
20
20
  // buttons stacked, the confirming one on top.
21
- // A button in slot="actions" with a `value` closes it with that value; so
22
- // does the submit of a <form method="dialog"> inside, with its submitter's
23
- // value. Before it closes, nk-close { value } fires and can be cancelled –
24
- // the place to check an input dialog; Escape and the backdrop close with ''.
21
+ // A button in slot="actions" with a `value` closes it with that value, and
22
+ // one anywhere inside with `data-close` with that attribute's value – a
23
+ // button with a value elsewhere, an option of <nk-segmented> say, leaves it
24
+ // open. The submit of a <form method="dialog"> inside closes it too, with
25
+ // its submitter's value. Before it closes, nk-close { value } fires and can
26
+ // be cancelled – the place to check an input dialog; Escape and the
27
+ // backdrop close with ''.
25
28
  // `returnValue` keeps the last value. `alert` makes it an alertdialog, named
26
29
  // and described by its title and text. `title` is the heading, never a
27
30
  // tooltip – see NkElement.takeTitle(). Put it directly under <body>.
@@ -91,11 +94,14 @@ class NkDialog extends NkElement {
91
94
  setupEvents() {
92
95
  this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(''); };
93
96
  this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(''); } };
94
- // A button with a value closes it; a submit button is the form's.
97
+ // A button in the actions with a value closes it, and one with data-close
98
+ // anywhere inside; a submit button is the form's.
95
99
  this._onClick = (e) => {
96
- const button = e.composedPath().find(n => n instanceof Element && n !== this && this.contains(n) && n.hasAttribute('value') && (n.localName === 'nk-btn' || n.localName === 'button'));
97
- if (!button || button.getAttribute('type') === 'submit') return;
98
- this.close(button.getAttribute('value'));
100
+ const path = e.composedPath(), own = path.slice(0, Math.max(0, path.indexOf(this))).filter(n => n instanceof Element && this.contains(n));
101
+ const button = own.find(n => n.localName === 'nk-btn' || n.localName === 'button');
102
+ if (!button || button.getAttribute('type') === 'submit' || button.hasAttribute('disabled')) return;
103
+ if (button.hasAttribute('data-close')) { this.close(button.getAttribute('data-close')); return; }
104
+ if (button.hasAttribute('value') && own.some(n => n.getAttribute('slot') === 'actions')) this.close(button.getAttribute('value'));
99
105
  };
100
106
  this._onSubmit = (e) => {
101
107
  if ((e.target.getAttribute('method') || '').toLowerCase() !== 'dialog') return;
@@ -10,12 +10,22 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
10
10
  // nk-field inside renders itself stacked and compact (12px label above a
11
11
  // full-width control) – the class markup does the same through
12
12
  // `.nk-fields > .nk-field`, which cannot cross the shadow boundary here.
13
+ // `fit` (NotionKit 1.11.0) lets the fields share the row instead of keeping
14
+ // 150px columns: two fields in a wide dialog are two halves.
13
15
  class NkFields extends NkElement {
16
+ static get observedAttributes() { return ['fit']; }
17
+
14
18
  render() {
15
- const grid = this.createElement('div', ['nk-fields']);
16
- grid.appendChild(document.createElement('slot'));
17
- this._wrapper.appendChild(grid);
19
+ this._grid = this.createElement('div', ['nk-fields']);
20
+ this._grid.appendChild(document.createElement('slot'));
21
+ this._wrapper.appendChild(this._grid);
22
+ this.onAttributeChanged();
18
23
  }
24
+
25
+ onAttributeChanged() { this._grid?.classList.toggle('fit', this.getBoolAttr('fit')); }
26
+
27
+ get fit() { return this.getBoolAttr('fit'); }
28
+ set fit(v) { this.setBoolAttr('fit', v); }
19
29
  }
20
30
 
21
31
  customElements.define('nk-fields', NkFields);
@@ -59,7 +59,7 @@ class NkFilterBar extends NkElement {
59
59
  setupEvents() {
60
60
  this._onClick = (e) => {
61
61
  const remove = e.target.closest('[data-remove]');
62
- if (remove) { this._filters.splice(Number(remove.dataset.remove), 1); this._sync(); this._emitChange(); return; }
62
+ if (remove) { const i = Number(remove.dataset.remove); this._filters = this._filters.filter((_, j) => j !== i); this._sync(); this._emitChange(); return; }
63
63
  const edit = e.target.closest('[data-edit]');
64
64
  if (edit) { const index = Number(edit.dataset.edit); this.emit('nk-action', { action: 'edit', index, filter: this._filters[index], anchor: edit }); return; }
65
65
  const btn = e.target.closest('[data-action]');
@@ -79,8 +79,10 @@ class NkFilterBar extends NkElement {
79
79
  _emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
80
80
  onAttributeChanged() { this._sync(); }
81
81
 
82
- get filters() { return this._filters; }
83
- set filters(v) { this._filters = Array.isArray(v) ? v : []; if (this._row) this._sync(); }
82
+ // A copy each way: the caller's array is never changed – removing a pill
83
+ // only reports the new list in nk-change.
84
+ get filters() { return [...this._filters]; }
85
+ set filters(v) { this._filters = Array.isArray(v) ? [...v] : []; if (this._row) this._sync(); }
84
86
  get value() { return this._input?.value ?? ''; }
85
87
  set value(v) { if (this._input) this._input.value = v ?? ''; }
86
88
  /**
@@ -1,7 +1,7 @@
1
1
  import { NkElement } from './base.js';
2
- import { r as renderPropertyCell } from './shared/property-cell-o7N30kAQ.js';
2
+ import { r as renderPropertyCell } from './shared/property-cell-Cddz5n2O.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
- import './shared/avatar-BiqCaHOt.js';
4
+ import './shared/avatar-CzPZN3uP.js';
5
5
 
6
6
  // <nk-list-view name="list" label="☰ List" meta-keys="due,status" new-row></nk-list-view>
7
7
  // The third database view: one line per row – icon and title, the `meta-keys`
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { i as initialsOf, p as paintAvatar } from './shared/avatar-BiqCaHOt.js';
2
+ import { i as initialsOf, p as paintAvatar } from './shared/avatar-CzPZN3uP.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
4
 
5
5
  // <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="green">
@@ -1,6 +1,6 @@
1
1
  import { NkElement } from './base.js';
2
2
  import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
3
- import { p as placeUnder } from './shared/floating-CxHxy-Cb.js';
3
+ import { p as placeUnder } from './shared/floating-CtqV6MBj.js';
4
4
  import '@jungherz-de/notionkit/notionkit-styles.js';
5
5
 
6
6
  // <nk-menu>
@@ -20,7 +20,7 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
20
20
  // close it too, a switch row does not. Opened from the keyboard, focus moves
21
21
  // to the first item; closing returns it to where it was.
22
22
  const floatSheet = new CSSStyleSheet();
23
- floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
23
+ floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none; }`);
24
24
 
25
25
  class NkMenu extends NkElement {
26
26
  static get hostStyles() { return floatSheet; }
@@ -44,8 +44,9 @@ class NkPage extends NkElement {
44
44
  }
45
45
 
46
46
  _sync() {
47
- const icon = this.getAttribute('icon');
48
- this._icon.textContent = icon || '';
47
+ const icon = this.getAttribute('icon') || '';
48
+ // Fallback content: write on a change only (see nk-switch).
49
+ if (this._icon.textContent !== icon) this._icon.textContent = icon;
49
50
  this._icon.style.display = icon ? '' : 'none';
50
51
  this._cover.style.display = this.getBoolAttr('cover') ? '' : 'none';
51
52
  // `covered` is the stylesheet's twin of `.nk-cover + .nk-page`: the icon
@@ -8,7 +8,9 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
8
8
  //
9
9
  // The light-DOM <option>s are copied into the shadow <select>. A
10
10
  // MutationObserver keeps the copy in step when a framework swaps them; the
11
- // empty string is a valid value; the live selection survives a rebuild.
11
+ // empty string is a valid value; the live selection survives a rebuild. A
12
+ // value set before its option exists – a framework sets properties before
13
+ // children – waits and is applied once the option arrives.
12
14
  class NkSelect extends NkFormElement {
13
15
  static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'wide', 'aria-label']; }
14
16
  static get observesLightDom() { return true; }
@@ -51,9 +53,11 @@ class NkSelect extends NkFormElement {
51
53
  this._select.innerHTML = '';
52
54
  for (const node of nodes) this._select.appendChild(node.cloneNode(true));
53
55
 
54
- // Keep the live selection when it survived; otherwise the value attribute;
55
- // otherwise the browser default (first option / `selected`).
56
- if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
56
+ // A value that waited for its option first; then the live selection when
57
+ // it survived; otherwise the value attribute; otherwise the browser
58
+ // default (first option / `selected`).
59
+ if (this._pending != null && this._applyValue(this._pending)) this._pending = null;
60
+ else if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
57
61
  this._syncFormValue();
58
62
  }
59
63
 
@@ -67,6 +71,7 @@ class NkSelect extends NkFormElement {
67
71
 
68
72
  setupEvents() {
69
73
  this._onChange = () => {
74
+ this._pending = null;
70
75
  this._syncFormValue();
71
76
  this.emit('nk-change', { value: this._select.value, name: this.name });
72
77
  this.dispatchEvent(new Event('change', { bubbles: true }));
@@ -107,10 +112,13 @@ class NkSelect extends NkFormElement {
107
112
 
108
113
  focus(options) { this._select?.focus(options); }
109
114
 
110
- get value() { return this._select?.value ?? ''; }
115
+ get value() { return this._pending ?? this._select?.value ?? ''; }
111
116
  set value(v) {
112
- if (this._select) { this._select.value = v; this._syncFormValue(); }
113
- else this.setAttribute('value', v);
117
+ const value = v == null ? '' : String(v);
118
+ if (!this._select) { this.setAttribute('value', value); return; }
119
+ // No option carries it yet: keep it until one does.
120
+ this._pending = this._applyValue(value) ? null : value;
121
+ this._syncFormValue();
114
122
  }
115
123
  get selectedIndex() { return this._select?.selectedIndex ?? -1; }
116
124
  get options() { return this._select?.options ?? []; }
@@ -37,7 +37,11 @@ class NkSwitch extends NkFormElement {
37
37
  }
38
38
 
39
39
  _syncText() {
40
- this._textNode.data = this.getAttribute('text') ?? '';
40
+ // Only on a change: WebKit fires slotchange for a write to the slot's
41
+ // fallback text, even the same text, and this runs on slotchange – an
42
+ // endless loop that froze Safari with an empty <nk-switch>.
43
+ const text = this.getAttribute('text') ?? '';
44
+ if (this._textNode.data !== text) this._textNode.data = text;
41
45
  const hasText = !!(this.getAttribute('text') || this._slot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.data.trim()));
42
46
  this._text.hidden = !hasText;
43
47
  this._row.style.display = hasText ? '' : 'contents';
@@ -1,7 +1,7 @@
1
1
  import { NkElement } from './base.js';
2
- import { c as compareBy, r as renderPropertyCell } from './shared/property-cell-o7N30kAQ.js';
2
+ import { c as compareBy, r as renderPropertyCell } from './shared/property-cell-Cddz5n2O.js';
3
3
  import '@jungherz-de/notionkit/notionkit-styles.js';
4
- import './shared/avatar-BiqCaHOt.js';
4
+ import './shared/avatar-CzPZN3uP.js';
5
5
 
6
6
  // <nk-table-view name="table" label="▦ Table" count new-row sortable wrap></nk-table-view>
7
7
  // `wrap` lets cell text break (`.nk-table.wrap`), like Notion's "wrap column".
@@ -1,5 +1,5 @@
1
1
  import { NkElement } from './base.js';
2
- import { a as placeNear } from './shared/floating-CxHxy-Cb.js';
2
+ import { a as placeNear } from './shared/floating-CtqV6MBj.js';
3
3
  import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
4
4
  import '@jungherz-de/notionkit/notionkit-styles.js';
5
5
 
@@ -15,12 +15,15 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
15
15
  // element. With `for` it belongs to the element with that id and shows its
16
16
  // own content and `shortcut`. It appears after `delay` ms (400) under the
17
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).
18
+ // blur, scrolling – the wheel too, inside a shadow root – and Escape hide
19
+ // it. It sits 6px below its target and centred, above it where the window
20
+ // ends – placement="top" prefers above – and 8px inside the window
21
+ // (placeNear in util/floating.js). show(target, text, shortcut) takes an
22
+ // element or a rect, for what has no element of its own: the bars of a
23
+ // Gantt chart; such a tooltip stays until hide(), the pointer on another
24
+ // [data-tooltip], a press or Escape. A line break in the text is kept.
25
+ // While it shows, the target's aria-describedby names it (in the same
26
+ // document or shadow root).
24
27
  const hostSheet = new CSSStyleSheet();
25
28
  hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
26
29
  let count = 0;
@@ -45,6 +48,7 @@ class NkTooltip extends NkElement {
45
48
  _fill(text, key = this.getAttribute('shortcut')) {
46
49
  this._slot.style.display = text == null ? '' : 'none';
47
50
  this._text.data = text ?? '';
51
+ this._bubble.classList.toggle('lines', (text ?? '').includes('\n'));
48
52
  this._key.textContent = key || '';
49
53
  this._key.hidden = !key;
50
54
  }
@@ -53,6 +57,9 @@ class NkTooltip extends NkElement {
53
57
  show(target, text, shortcut) {
54
58
  clearTimeout(this._timer);
55
59
  if (!target) return;
60
+ // What the pointer is on now: a rect has no element, so moving over the
61
+ // page leaves a tooltip shown for one alone.
62
+ this._target = target instanceof Element ? target : null;
56
63
  this._fill(text ?? null, shortcut);
57
64
  this._describe(target);
58
65
  placeNear(this, target, this.getAttribute('placement') === 'top' ? 'top' : 'bottom');
@@ -117,6 +124,8 @@ class NkTooltip extends NkElement {
117
124
  document.addEventListener('focusout', this._onHide);
118
125
  document.addEventListener('pointerdown', this._onHide, true);
119
126
  document.addEventListener('scroll', this._onHide, true);
127
+ // Scroll events do not leave a shadow root; the wheel does.
128
+ document.addEventListener('wheel', this._onHide, { capture: true, passive: true });
120
129
  document.addEventListener('keydown', this._onKey);
121
130
  }
122
131
 
@@ -126,6 +135,7 @@ class NkTooltip extends NkElement {
126
135
  document.removeEventListener('focusout', this._onHide);
127
136
  document.removeEventListener('pointerdown', this._onHide, true);
128
137
  document.removeEventListener('scroll', this._onHide, true);
138
+ document.removeEventListener('wheel', this._onHide, { capture: true });
129
139
  document.removeEventListener('keydown', this._onKey);
130
140
  this.hide();
131
141
  }
@@ -60,7 +60,10 @@ class NkTreeItem extends NkElement {
60
60
  else if (slot === 'end') end.push(node);
61
61
  else if (node.localName === 'nk-tree-item') children.push(node);
62
62
  else label.push(node);
63
- } else if (node.nodeType === Node.TEXT_NODE && node.data.trim()) {
63
+ } else if (node.nodeType === Node.TEXT_NODE) {
64
+ // Every text node, empty and blank ones too: a template engine fills
65
+ // an expression's node after the item is built ("${i} ${title}"),
66
+ // and the blank node between two expressions is the space.
64
67
  label.push(node);
65
68
  }
66
69
  }
@@ -83,9 +86,12 @@ class NkTreeItem extends NkElement {
83
86
  if (this._hasChildren) this._row.setAttribute('aria-expanded', open ? 'true' : 'false');
84
87
  else this._row.removeAttribute('aria-expanded');
85
88
  this._children.classList.toggle('collapsed', !open || !this._hasChildren);
86
- this._icon.textContent = this.getAttribute('icon') || '';
87
- this._icon.style.display = this.getAttribute('icon') ? '' : 'none';
88
- this._labelText.data = this.getAttribute('label') || '';
89
+ // Fallback content: write on a change only (see nk-switch).
90
+ const icon = this.getAttribute('icon') || '';
91
+ if (this._icon.textContent !== icon) this._icon.textContent = icon;
92
+ this._icon.style.display = icon ? '' : 'none';
93
+ const label = this.getAttribute('label') || '';
94
+ if (this._labelText.data !== label) this._labelText.data = label;
89
95
  // .actions is display:none until hover (a hover-parent rule); `hidden`
90
96
  // would lose to that rule, so the block is detached instead.
91
97
  const wantActions = !this.getBoolAttr('no-actions') && !this._hasEnd;
@@ -13,7 +13,7 @@ function paintAvatar(el, color) {
13
13
  el.style.background = color && !AVATAR_COLORS.includes(color) ? color : '';
14
14
  }
15
15
 
16
- /** "Sara Lindt" → "SL". */
17
- const initialsOf = name => String(name || '').split(/\s+/).map(w => w[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
16
+ /** "Sara Lindt" → "SL", "Planer (Dev)" → "PD", "Anna-Lena Groß" → "AG": the first letter or digit of each word; a word without one is skipped. */
17
+ const initialsOf = name => String(name || '').split(/\s+/).map(w => w.match(/[\p{L}\p{N}]/u)?.[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
18
18
 
19
19
  export { initialsOf as i, paintAvatar as p };
@@ -0,0 +1,66 @@
1
+ // Placement for elements that float over the page – <nk-menu floating>,
2
+ // <nk-calendar floating>, <nk-tooltip>: the host is a fixed box put under
3
+ // its anchor. A transformed ancestor – an open nk-modal or nk-dialog –
4
+ // becomes the containing block of a fixed box, so the frame it really sits
5
+ // in is measured and taken off.
6
+
7
+ /** The border box of an element, or of the first descendant with one (display: contents hosts). */
8
+ function boxOf(el) {
9
+ if (el.getClientRects().length) return el.getBoundingClientRect();
10
+ for (const child of [...(el.shadowRoot?.children ?? []), ...el.children]) {
11
+ const r = boxOf(child);
12
+ if (r.width || r.height) return r;
13
+ }
14
+ return el.getBoundingClientRect();
15
+ }
16
+
17
+ /**
18
+ * Where a fixed `host` really sits: the viewport position of its containing
19
+ * block's left, top and right edges – the viewport's own, unless a
20
+ * transformed ancestor takes its place – and the host's size.
21
+ */
22
+ function frameOf(host) {
23
+ const s = host.style;
24
+ s.top = '0px'; s.left = '0px'; s.right = 'auto';
25
+ const a = host.getBoundingClientRect();
26
+ s.left = 'auto'; s.right = '0px';
27
+ const b = host.getBoundingClientRect();
28
+ return { x: a.left, y: a.top, right: b.right, width: a.width, height: a.height };
29
+ }
30
+
31
+ /**
32
+ * Puts `host` 6px below `target` – an element or a rect ({ left, top, right,
33
+ * bottom }, a DOMRect) – centred on it: above with side="top", or where the
34
+ * window ends below; kept 8px inside the window. For <nk-tooltip>.
35
+ */
36
+ function placeNear(host, target, side = 'bottom', gap = 6) {
37
+ const r = target instanceof Element ? boxOf(target) : target;
38
+ const f = frameOf(host), w = f.width, h = f.height;
39
+ const below = r.bottom + gap, above = r.top - gap - h;
40
+ const top = side === 'top' ? (above < 8 ? below : above) : (below + h > innerHeight - 8 ? above : below);
41
+ const left = Math.min(Math.max(8, (r.left + r.right) / 2 - w / 2), innerWidth - 8 - w);
42
+ host.style.top = `${Math.round(top - f.y)}px`;
43
+ host.style.left = `${Math.round(left - f.x)}px`;
44
+ host.style.right = 'auto';
45
+ }
46
+
47
+ /**
48
+ * Puts `host` 6px under `anchor`, right edges aligned – left edges with
49
+ * align="start". Where the window ends below and there is more room above,
50
+ * it opens upwards; either way --_nk-float-max caps its height to the room
51
+ * there, and a longer menu scrolls.
52
+ */
53
+ function placeUnder(host, anchor, align) {
54
+ const r = boxOf(anchor), gap = 6, edge = 8;
55
+ host.style.removeProperty('--_nk-float-max');
56
+ const f = frameOf(host);
57
+ const roomBelow = innerHeight - r.bottom - gap - edge, roomAbove = r.top - gap - edge;
58
+ const up = f.height > roomBelow && roomAbove > roomBelow;
59
+ const room = Math.max(0, Math.floor(up ? roomAbove : roomBelow));
60
+ if (f.height > room) host.style.setProperty('--_nk-float-max', `${room}px`);
61
+ host.style.top = `${(up ? r.top - gap - Math.min(f.height, room) : r.bottom + gap) - f.y}px`;
62
+ if (align === 'start') { host.style.left = `${r.left - f.x}px`; host.style.right = 'auto'; }
63
+ else { host.style.right = `${f.right - Math.min(r.right, innerWidth - edge)}px`; host.style.left = 'auto'; }
64
+ }
65
+
66
+ export { placeNear as a, placeUnder as p };
@@ -1,4 +1,4 @@
1
- import { i as initialsOf, p as paintAvatar } from './avatar-BiqCaHOt.js';
1
+ import { i as initialsOf, p as paintAvatar } from './avatar-CzPZN3uP.js';
2
2
 
3
3
  // Polymorphic property renderer for database views. Returns plain DOM built
4
4
  // from NotionKit classes – deliberately not a custom element: every cell rule