@jungherz-de/notionkit-elements 1.19.0 → 1.20.1

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 CHANGED
@@ -4,6 +4,66 @@ All notable changes to NotionKit Elements are documented here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.20.1] – 2026-10-14
8
+
9
+ Elements only; NotionKit CSS stays at 1.20.0 (peer `>= 1.20.0`).
10
+
11
+ ### Fixed
12
+ - **`<nk-tooltip>` follows the pointer and the focus inside a shadow root
13
+ (#3).** Moving from one `[data-tooltip]` to the next in the same shadow
14
+ root never updated the tooltip – the first one stayed, or none appeared –
15
+ for the pointer and for Tab alike; `<nk-copy-field icons secret>` kept
16
+ saying "Show" over Copy, an app's toolbar or chart bars in one root did
17
+ the same. `pointerover`, `focusin` and `focusout` carry a `relatedTarget`,
18
+ and when target and `relatedTarget` share a shadow tree the DOM standard
19
+ ends the event path at its host, so the listeners on the document never
20
+ heard of the move. The tooltip now also reads `pointermove` (no
21
+ `relatedTarget`, it always reaches the document) and acts only when the
22
+ element under the pointer changes, so a hand resting on the page leaves a
23
+ tooltip the keyboard or `show(rect)` opened alone; and it catches a key
24
+ that moved the focus – Tab, Option+Tab in Safari, the arrow keys of a
25
+ roving tab stop that moves the focus in its own `keydown` – on `keyup`,
26
+ against the focus recorded before the key went down. Nested shadow roots
27
+ are followed too. A focus move by script alone inside one shadow root
28
+ stays unseen. Reported by Mailverse.
29
+ - **Leaving the window** over a `[data-tooltip]` left its tooltip standing,
30
+ and one still waiting appeared after the pointer had gone. It now goes
31
+ with the pointer (`pointerout` without a `relatedTarget`); a tooltip the
32
+ keyboard or `show(rect)` opened is not the pointer's and stays. Found
33
+ while testing #3.
34
+
35
+ ## [1.20.0] – 2026-10-12
36
+
37
+ Built against NotionKit CSS 1.20.0 (peer `>= 1.20.0`).
38
+
39
+ ### Added
40
+ - **`<nk-theme-toggle modes="auto light dark">` – a third state that follows
41
+ the system.** `auto` is what no stored value means, so the documented
42
+ inline script before first paint keeps working and a choice made without
43
+ `modes` still counts; while it is in force the element follows
44
+ `prefers-color-scheme` live, which the toggle never did. A click goes from
45
+ `auto` to the theme *not* on screen, then to the other one, then back, so
46
+ every click but the last changes something visibly. The glyph shows the
47
+ mode – `.nk-theme-glyph`, sun and moon diagonally for `auto` – instead of
48
+ the next theme, and the title names the mode and the next step (four new
49
+ texts: `appearance`, `themeAuto`, `themeLight`, `themeDark`). A choice made
50
+ in another tab arrives through the `storage` event. `value` returns
51
+ `auto | light | dark`, the new `theme` what is on screen, `nk-change`
52
+ reports both; `nextMode()` and `themeFor(mode)` are public.
53
+ **Two states stay the default**, where nothing about this element changes.
54
+ Requested by Mailverse (#2).
55
+ - `theme-color` keeps `<meta name="theme-color">` in step: while a theme is
56
+ fixed every bar carries that theme's colour, back on `auto` each one
57
+ carries its own again. The colours are the page's, never invented.
58
+
59
+ ### Fixed
60
+ - The toggle added its system-preference listener to a throwaway
61
+ `matchMedia()` object and removed it from a second one, so it was never
62
+ released and could be collected with its query. One query per element now.
63
+ - A mode whose write failed (Safari's private mode refuses to store but
64
+ reads) is kept in the element until the page reloads, instead of falling
65
+ back to `auto` on the next read.
66
+
7
67
  ## [1.19.0] – 2026-09-25
8
68
 
9
69
  Built against NotionKit CSS 1.19.1 (peer `>= 1.19.1`). Auxdesk's wish 24 and what SupaGantt's mock-ups found – 14, 15 and 16 – together with a toast that never hid.
@@ -746,6 +806,8 @@ built and tested against NotionKit CSS 1.1.1 (peer range `>= 1.0.0`).
746
806
  import, never in the core bundle – shadow-less, adding `nk-block-host` to
747
807
  itself so the foundation's editor adapter rules apply.
748
808
 
809
+ [1.20.1]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.1
810
+ [1.20.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.0
749
811
  [1.19.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.19.0
750
812
  [1.18.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.18.0
751
813
  [1.17.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.17.0
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.19.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.20.1-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-88-529cca?style=flat-square" alt="88 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.19.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.20.1-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
 
@@ -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.19.1/notionkit.min.css">
75
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/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">
@@ -161,6 +161,10 @@ The elements on the page take new texts at once, after `setStrings()` and when `
161
161
  | `commandPlaceholder` | `nk-cmdk` | `placeholder` | Search or type a command … | Suchen oder Befehl eingeben … |
162
162
  | `emojiPlaceholder` | `nk-emoji-picker` | `placeholder` | Search… | Suchen … |
163
163
  | `toggleTheme` | `nk-theme-toggle` | `title` | Toggle light / dark | Hell / Dunkel |
164
+ | `appearance` | `nk-theme-toggle` (`modes` with `auto`) | `title` | Appearance: {mode} – click: {next} | Darstellung: {mode} – Klick: {next} |
165
+ | `themeAuto` | `nk-theme-toggle` (`modes` with `auto`) | – | automatic (follows the system) | automatisch (wie das System) |
166
+ | `themeLight` | `nk-theme-toggle` (`modes` with `auto`) | – | light | hell |
167
+ | `themeDark` | `nk-theme-toggle` (`modes` with `auto`) | – | dark | dunkel |
164
168
  | `synced` | `nk-synced` | `badge` | ⟳ synced | ⟳ synchronisiert |
165
169
  | `message` | `nk-ai-input-row` | – | Message | Nachricht |
166
170
  | `comment` | `nk-comments` | – | Comment | Kommentar |
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: notionkit-elements
3
- description: NotionKit Elements is a vanilla-JS Web Components library (v1.19.0) wrapping NotionKit CSS v1.19.1 – the calm, document-centric design system in the Notion idiom. 88 custom elements with the `nk-` prefix, Shadow DOM, automatic light/dark sync via data-theme on <html>, and form-associated controls. Use this reference whenever generating HTML that uses <nk-*> tags to get attributes, slots, events and composition right.
3
+ description: NotionKit Elements is a vanilla-JS Web Components library (v1.20.1) wrapping NotionKit CSS v1.20.0 – the calm, document-centric design system in the Notion idiom. 88 custom elements with the `nk-` prefix, Shadow DOM, automatic light/dark sync via data-theme on <html>, and form-associated controls. Use this reference whenever generating HTML that uses <nk-*> tags to get attributes, slots, events and composition right.
4
4
  ---
5
5
 
6
6
  # NotionKit Elements – AI Component Reference
@@ -21,8 +21,8 @@ description: NotionKit Elements is a vanilla-JS Web Components library (v1.19.0)
21
21
  <head>
22
22
  <meta charset="UTF-8">
23
23
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
25
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
24
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
25
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
26
26
  </head>
27
27
  <body class="nk-body">
28
28
  <nk-btn variant="primary">Save</nk-btn>
@@ -1177,7 +1177,7 @@ Give it plain `<span>` or `<a>` children; they are cloned into the bar with sepa
1177
1177
 
1178
1178
  ### 3.33 `<nk-theme-toggle>` – Theme toggle
1179
1179
 
1180
- The ☀️/🌙 button. Flips `data-theme` on `<html>`, remembers the choice in `localStorage`, applies a stored or system preference on first connect when `<html>` has no theme yet, and accepts `postMessage({ nkTheme })` from a parent page. `apply(theme)` does everything a click does: sets, persists and fires `nk-change`.
1180
+ The ☀️/🌙 button. Flips `data-theme` on `<html>`, remembers the choice in `localStorage`, applies a stored or system preference on first connect when `<html>` has no theme yet, and accepts `postMessage({ nkTheme })` from a parent page. `apply(mode)` does everything a click does: sets, persists and fires `nk-change`.`modes="auto light dark"` adds a third state: auto follows `prefers-color-scheme`, live, and is what no stored value means – so the inline script before first paint keeps working and a choice made without `modes` still counts. A click then goes from auto to the theme not on screen, to the other one, back to auto, so every click but the last changes something. The glyph shows the mode (`nk-theme-glyph`, sun and moon diagonally for auto) instead of the next theme, the title names the mode and the next one, and another tab's choice arrives through the `storage` event. Two states stay the default, where nothing about this element changes.
1181
1181
 
1182
1182
  ```html
1183
1183
  <nk-theme-toggle></nk-theme-toggle>
@@ -1185,14 +1185,16 @@ The ☀️/🌙 button. Flips `data-theme` on `<html>`, remembers the choice in
1185
1185
 
1186
1186
  | Attribute | Type | Default | Description |
1187
1187
  |---|---|---|---|
1188
- | `storage-key` | string | `nk-theme` | localStorage key. |
1189
- | `title` | string | – | Tooltip. |
1188
+ | `modes` | string | `light dark` | The states offered, e.g. `auto light dark`; without `auto` among them nothing changes. |
1189
+ | `storage-key` | string | `nk-theme` | localStorage key; `auto` is no entry. |
1190
+ | `theme-color` | boolean | – | Keeps `<meta name="theme-color">` in step: while a theme is fixed every bar carries that theme's colour – the page's own, never invented. |
1191
+ | `title` | string | – | Tooltip; replaces the built-in text. |
1190
1192
 
1191
- **Events:** `nk-change` `{ value: 'light' | 'dark' }` – Theme applied.
1193
+ **Events:** `nk-change` `{ value: 'auto' | 'light' | 'dark', theme: 'light' | 'dark' }` – Mode applied; `theme` is what is on screen (without `auto` both are the theme).
1192
1194
 
1193
- **Properties:** `value` · **Methods:** `apply(theme)`
1195
+ **Properties:** `value`, `theme`, `mode`, `modes` · **Methods:** `apply(mode)`, `nextMode()`, `themeFor(mode)`
1194
1196
 
1195
- **Replaces:** `.nk-theme-toggle`
1197
+ **Replaces:** `.nk-theme-toggle`, `.nk-theme-glyph`
1196
1198
 
1197
1199
  ```html
1198
1200
  <!-- equivalent class markup -->
@@ -2560,7 +2562,7 @@ One inverted pill at the bottom centre, above every overlay; over a tab bar at t
2560
2562
 
2561
2563
  ### 3.76 `<nk-tooltip>` – Tooltip
2562
2564
 
2563
- Notion’s hover hint in the toast’s colours, a shortcut muted beside it. One `<nk-tooltip>` without `for` serves every element with `data-tooltip` (and `data-tooltip-key`) on the page, inside shadow roots too – a whole toolbar in one element; with `for` it belongs to one element and shows its own content and `shortcut`. It appears after `delay` ms (400) under the pointer and at once on keyboard focus, never on touch; leaving, a press, blur, scrolling and Escape hide it. It sits 6px below its target and centred, above it where the window ends, 8px inside the window – `placeNear` in `util/floating.js`. `show(target, text, shortcut)` takes an element or a rect, for what has no element of its own: the bars of a chart. The target’s `aria-describedby` names it while it shows. Hover the buttons. A tooltip shown with `show(rect, …)` stays until `hide()`, the pointer on another `[data-tooltip]`, a press or Escape; the wheel hides it too, also inside a shadow root. A line break in the text is kept.
2565
+ Notion’s hover hint in the toast’s colours, a shortcut muted beside it. One `<nk-tooltip>` without `for` serves every element with `data-tooltip` (and `data-tooltip-key`) on the page, inside shadow roots too – a whole toolbar in one element, and it follows pointer and keyboard from one such element to the next within the same shadow root; with `for` it belongs to one element and shows its own content and `shortcut`. It appears after `delay` ms (400) under the pointer and at once on keyboard focus, never on touch; leaving – the window too –, a press, blur, scrolling and Escape hide it. It sits 6px below its target and centred, above it where the window ends, 8px inside the window – `placeNear` in `util/floating.js`. `show(target, text, shortcut)` takes an element or a rect, for what has no element of its own: the bars of a chart. The target’s `aria-describedby` names it while it shows. Hover the buttons. A tooltip shown with `show(rect, …)` stays until `hide()`, the pointer on another `[data-tooltip]`, a press or Escape; the wheel hides it too, also inside a shadow root. A line break in the text is kept.
2564
2566
 
2565
2567
  ```html
2566
2568
  <div style="display:flex;gap:16px;align-items:center;padding:4px 0 44px">
@@ -3150,8 +3152,8 @@ Eight skeletons, one per app shape, mirroring the NotionKit CSS SKILL.md. Copy o
3150
3152
  <html lang="en" data-theme="light">
3151
3153
  <head>
3152
3154
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3153
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3154
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3155
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3156
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3155
3157
  </head>
3156
3158
  <body class="nk-body">
3157
3159
  <nk-app>
@@ -3232,8 +3234,8 @@ Rules of the shell: `nk-sidebar`, `nk-topbar` and `nk-page` are `display: conten
3232
3234
  <html lang="en" data-theme="light">
3233
3235
  <head>
3234
3236
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3235
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3236
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3237
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3238
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3237
3239
  </head>
3238
3240
  <body class="nk-body">
3239
3241
  <nk-app>
@@ -3340,8 +3342,8 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3340
3342
  <html lang="en" data-theme="light">
3341
3343
  <head>
3342
3344
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3343
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3344
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3345
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3346
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3345
3347
  </head>
3346
3348
  <body class="nk-body">
3347
3349
  <nk-app>
@@ -3400,8 +3402,8 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3400
3402
  <html lang="en" data-theme="light">
3401
3403
  <head>
3402
3404
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3403
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3404
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3405
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3406
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3405
3407
  </head>
3406
3408
  <body class="nk-body">
3407
3409
  <!-- your app -->
@@ -3478,8 +3480,8 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3478
3480
  <html lang="en" data-theme="light">
3479
3481
  <head>
3480
3482
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3481
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3482
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3483
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3484
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3483
3485
  </head>
3484
3486
  <body class="nk-body">
3485
3487
  <div class="nk-page" style="padding-top:48px">
@@ -3525,8 +3527,8 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3525
3527
  <html lang="en" data-theme="light">
3526
3528
  <head>
3527
3529
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3528
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3529
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3530
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3531
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3530
3532
  </head>
3531
3533
  <body class="nk-body">
3532
3534
  <nk-page narrow icon="📘" cover>
@@ -3570,8 +3572,8 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3570
3572
  <html lang="en" data-theme="light">
3571
3573
  <head>
3572
3574
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3573
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3574
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3575
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3576
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3575
3577
  </head>
3576
3578
  <body class="nk-body">
3577
3579
  <nk-app>
@@ -3625,8 +3627,8 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3625
3627
  <html lang="en" data-theme="light">
3626
3628
  <head>
3627
3629
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3628
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.19.1/notionkit.min.css">
3629
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.19.0/dist/notionkit-elements.min.js"></script>
3630
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3631
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3630
3632
  </head>
3631
3633
  <body class="nk-body">
3632
3634
  <!-- A narrow column, centred: layout is yours, so it is inline. -->
@@ -3761,7 +3763,7 @@ Form controls additionally re-dispatch a native, bubbling `change` event, so `fo
3761
3763
  | `<nk-tree-item>` | shell | `icon`, `label`, `value`, `href` | `(default)`, `icon`, `end` | `nk-select`, `nk-toggle`, `nk-action` |
3762
3764
  | `<nk-topbar>` | shell | – | `(default)`, `actions` | – |
3763
3765
  | `<nk-breadcrumb>` | shell | `separator` | `(default)` | `nk-select` |
3764
- | `<nk-theme-toggle>` | shell | `storage-key`, `title` | – | `nk-change` |
3766
+ | `<nk-theme-toggle>` | shell | `modes`, `storage-key`, `theme-color`, `title` | – | `nk-change` |
3765
3767
  | `<nk-tab-bar>` | shell | `value`, `always`, `fixed`, `floating` | `(default)` | `nk-change`, `nk-select` |
3766
3768
  | `<nk-tab-bar-item>` | shell | `icon`, `value`, `label`, `href` | `(default)`, `icon` | `nk-select` |
3767
3769
  | `<nk-page>` | page | `icon`, `full`, `small`, `cover` | `(default)`, `cover`, `icon` | `nk-action` |
@@ -3837,10 +3839,10 @@ Form controls additionally re-dispatch a native, bubbling `change` event, so `fo
3837
3839
  | Texts | `src/util/strings.js` – the English and German dictionary and `setStrings()`; `NkElement#str(key)` reads it in the element's language, `onStringsChanged()` runs after `setStrings()` and when `<html lang>` changes |
3838
3840
  | Components | `src/components/{forms,content,shell,page,overlays,data}/nk-*.js`, one tag per file, `customElements.define` at the bottom |
3839
3841
  | Build | Rollup: IIFE, minified IIFE, ESM, and per-component ESM entries on a stable `dist/components/base.js` that import NotionKit's sheet (`@jungherz-de/notionkit/notionkit-styles.js`) instead of inlining it; the full bundles inline it and export `componentsSheet` |
3840
- | Peer | `@jungherz-de/notionkit >= 1.19.0` – from 1.5.0 on the elements and the foundation share one version number; the bundle embeds that release's stylesheet, so keep them in step |
3842
+ | Peer | `@jungherz-de/notionkit >= 1.20.1` – from 1.5.0 on the elements and the foundation share one version number; the bundle embeds that release's stylesheet, so keep them in step |
3841
3843
 
3842
3844
  Lifecycle: construct (attach shadow, adopt sheets) → first connect (wrapper + `render()`) → every connect (`setupEvents()`, theme registration, light-DOM observer) → `attributeChangedCallback` → `onAttributeChanged` → disconnect (`teardownEvents()`, unregister).
3843
3845
 
3844
3846
 
3845
3847
  ---
3846
- *NotionKit Elements v1.19.0 · wrapping NotionKit CSS v1.19.1 · MIT · Jungherz GmbH*
3848
+ *NotionKit Elements v1.20.1 · wrapping NotionKit CSS v1.20.0 · MIT · Jungherz GmbH*
@@ -18,6 +18,7 @@ const BUILT_IN = {
18
18
  commentPlaceholder: 'Comment …', send: 'Send', askPlaceholder: 'Ask something …',
19
19
  commandPlaceholder: 'Search or type a command …', emojiPlaceholder: 'Search…',
20
20
  toggleTheme: 'Toggle light / dark', synced: '⟳ synced',
21
+ appearance: 'Appearance: {mode} – click: {next}', themeAuto: 'automatic (follows the system)', themeLight: 'light', themeDark: 'dark',
21
22
  message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
22
23
  searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
23
24
  add: 'Add', more: 'More',
@@ -32,6 +33,7 @@ const BUILT_IN = {
32
33
  commentPlaceholder: 'Kommentieren …', send: 'Senden', askPlaceholder: 'Frag etwas …',
33
34
  commandPlaceholder: 'Suchen oder Befehl eingeben …', emojiPlaceholder: 'Suchen …',
34
35
  toggleTheme: 'Hell / Dunkel', synced: '⟳ synchronisiert',
36
+ appearance: 'Darstellung: {mode} – Klick: {next}', themeAuto: 'automatisch (wie das System)', themeLight: 'hell', themeDark: 'dunkel',
35
37
  message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
36
38
  searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
37
39
  add: 'Hinzufügen', more: 'Mehr',
@@ -8,62 +8,203 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
8
8
  // system preference on first connect when <html> carries no theme yet, and
9
9
  // accepts postMessage({ nkTheme }) from a parent page (the landing page's
10
10
  // embedded demo uses that).
11
- class NkThemeToggle extends NkElement {
12
- static get observedAttributes() { return ['storage-key', 'title'];
11
+ //
12
+ // <nk-theme-toggle modes="auto light dark"> adds a third state (1.20.0):
13
+ // `auto` follows prefers-color-scheme, live, and is what no stored value
14
+ // means – so the documented inline script before first paint keeps working
15
+ // and a choice made without `modes` still counts. The glyph then shows the
16
+ // mode (☀️, 🌙, both for auto) instead of the next theme, the title names
17
+ // the mode and the next one, other tabs follow through the storage event,
18
+ // and `theme-color` keeps <meta name="theme-color"> in step. Two states stay
19
+ // the default, where nothing about this element changes.
20
+ const GLYPH = { light: '☀️', dark: '🌙' };
13
21
 
14
- }
22
+ class NkThemeToggle extends NkElement {
23
+ static get observedAttributes() { return ['storage-key', 'title', 'modes', 'theme-color']; }
15
24
 
16
25
  render() {
17
26
  this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
18
- this._syncTitle();
19
27
  this._wrapper.appendChild(this._btn);
20
- const root = document.documentElement;
28
+ this._readThemeColors();
29
+ if (this.auto) {
30
+ // With `auto` this element owns the theme: a stored choice, else the
31
+ // system. The inline script before first paint has set the same value.
32
+ this._applyMode();
33
+ } else {
34
+ const root = document.documentElement;
35
+ let stored = null;
36
+ try { stored = localStorage.getItem(this.storageKey); } catch {}
37
+ if (stored === 'light' || stored === 'dark') root.setAttribute('data-theme', stored);
38
+ else if (!root.hasAttribute('data-theme')) root.setAttribute('data-theme', this.systemTheme);
39
+ this._syncThemeColor();
40
+ this._syncGlyph();
41
+ this._syncTitle();
42
+ }
43
+ }
44
+
45
+ // ── Modes ──
46
+
47
+ /** The states offered, in the author's order; without a theme among them the default two. */
48
+ get modes() {
49
+ const list = [...new Set((this.getAttribute('modes') || '').toLowerCase().split(/\s+/))].filter(m => m === 'auto' || m === 'light' || m === 'dark');
50
+ return list.some(m => m !== 'auto') ? list : ['light', 'dark'];
51
+ }
52
+
53
+ /** Whether `auto` is offered – the only thing that changes this element's behaviour. */
54
+ get auto() { return this.modes.includes('auto'); }
55
+
56
+ /** One query object per element: a listener on a throwaway one is neither removable nor safe from collection. */
57
+ get systemQuery() { return (this._query ??= matchMedia('(prefers-color-scheme: dark)')); }
58
+
59
+ get systemTheme() { return this.systemQuery.matches ? 'dark' : 'light'; }
60
+
61
+ /** The mode in force: 'auto' (nothing stored), 'light' or 'dark'. Without `auto` the theme on screen. */
62
+ get mode() {
63
+ if (!this.auto) return getCurrentTheme();
21
64
  let stored = null;
22
- try { stored = localStorage.getItem(this.storageKey); } catch {}
23
- if (stored === 'light' || stored === 'dark') root.setAttribute('data-theme', stored);
24
- else if (!root.hasAttribute('data-theme')) root.setAttribute('data-theme', matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
65
+ try { stored = localStorage.getItem(this.storageKey); } catch { return this._unsaved || 'auto'; }
66
+ if (stored === 'light' || stored === 'dark') return stored;
67
+ return this._unsaved || 'auto';
68
+ }
69
+
70
+ /** The theme on screen for a mode: `auto` is whatever the system says. */
71
+ themeFor(mode = this.mode) { return mode === 'auto' ? this.systemTheme : mode; }
72
+
73
+ /**
74
+ * The mode a click moves to. From `auto` to the theme that is *not* on
75
+ * screen, then to the other one, then back to `auto` – so every click but
76
+ * the last changes something visibly. Without `auto` it cycles the themes.
77
+ */
78
+ nextMode(mode = this.mode) {
79
+ const modes = this.modes, themes = modes.filter(m => m !== 'auto'), system = this.systemTheme;
80
+ if (!modes.includes('auto')) return themes[(themes.indexOf(mode) + 1) % themes.length];
81
+ if (mode === 'auto') return themes.find(m => m !== system) ?? themes[0];
82
+ if (mode !== system) return themes.includes(system) ? system : 'auto';
83
+ return 'auto';
84
+ }
85
+
86
+ // ── Applying ──
87
+
88
+ _applyMode(mode = this.mode) {
89
+ const theme = this.themeFor(mode);
90
+ const root = document.documentElement;
91
+ // Only on a change: the observer in base.js mirrors data-theme into every
92
+ // element, and auto re-applies the same value on every system change.
93
+ if (root.getAttribute('data-theme') !== theme) root.setAttribute('data-theme', theme);
94
+ this._syncThemeColor(mode);
25
95
  this._syncGlyph();
96
+ this._syncTitle();
97
+ }
98
+
99
+ /** The page's own theme-color values, per medium, before this element touched them. */
100
+ _readThemeColors() {
101
+ this._bars = [...document.querySelectorAll('meta[name="theme-color"]')].map(meta => ({
102
+ meta, dark: (meta.getAttribute('media') || '').includes('dark'), content: meta.getAttribute('content'),
103
+ }));
104
+ }
105
+
106
+ /**
107
+ * `theme-color`: while a theme is fixed every meta carries the colour of
108
+ * that theme, so the browser bar follows the choice; back on `auto` each
109
+ * one carries its own again. The colours are the page's – never invented.
110
+ */
111
+ _syncThemeColor(mode = this.mode) {
112
+ if (!this.hasAttribute('theme-color') || !this._bars?.length) return;
113
+ const auto = mode === 'auto', dark = this.themeFor(mode) === 'dark';
114
+ const fixed = (this._bars.find(bar => bar.dark === dark) ?? this._bars[0]).content;
115
+ for (const bar of this._bars) if (bar.content !== null) bar.meta.setAttribute('content', auto ? bar.content : fixed);
26
116
  }
27
117
 
28
118
  _syncGlyph() {
29
- if (this._btn) this._btn.textContent = getCurrentTheme() === 'dark' ? '☀️' : '🌙';
119
+ if (!this._btn) return;
120
+ if (!this.auto) {
121
+ // Two states: the glyph is the next theme, as it has always been.
122
+ this._btn.replaceChildren(getCurrentTheme() === 'dark' ? GLYPH.light : GLYPH.dark);
123
+ this._btn.removeAttribute('aria-label');
124
+ return;
125
+ }
126
+ const mode = this.mode;
127
+ const glyph = this.createElement('span', ['nk-theme-glyph'], { 'aria-hidden': 'true' });
128
+ if (mode === 'auto') glyph.classList.add('auto');
129
+ for (const text of mode === 'auto' ? [GLYPH.light, GLYPH.dark] : [GLYPH[mode]]) {
130
+ glyph.appendChild(Object.assign(document.createElement('span'), { textContent: text }));
131
+ }
132
+ this._btn.replaceChildren(glyph);
133
+ // Two emojis are no accessible name; the title's text is.
134
+ this._btn.setAttribute('aria-label', this._titleText());
30
135
  }
31
136
 
32
137
  _syncTheme(theme) {
33
138
  super._syncTheme(theme);
34
139
  this._syncGlyph();
140
+ this._syncTitle();
141
+ }
142
+
143
+ _titleText() {
144
+ const own = this.getAttribute('title');
145
+ if (own) return own;
146
+ if (!this.auto) return this.str('toggleTheme');
147
+ const name = (mode) => this.str(mode === 'auto' ? 'themeAuto' : mode === 'dark' ? 'themeDark' : 'themeLight');
148
+ return this.str('appearance').replace('{mode}', name(this.mode)).replace('{next}', name(this.nextMode()));
35
149
  }
36
150
 
151
+ _syncTitle() { if (this._btn) this._btn.title = this._titleText(); }
152
+ onStringsChanged() { this._syncTitle(); if (this.auto) this._syncGlyph(); }
153
+
37
154
  setupEvents() {
38
- this._onClick = () => this.apply(getCurrentTheme() === 'dark' ? 'light' : 'dark');
39
- this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark') this.apply(t); };
155
+ this._onClick = () => this.apply(this.nextMode());
156
+ this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark' || (t === 'auto' && this.auto)) this.apply(t); };
157
+ // `auto` has to notice a system switch while the page is open, and a
158
+ // choice made in another tab. Both only matter where `auto` is offered.
159
+ this._onSystem = () => { if (this.mode === 'auto') this._applyMode(); };
160
+ this._onStorage = (e) => { if (e.key === this.storageKey || e.key === null) this._applyMode(); };
40
161
  this._btn.addEventListener('click', this._onClick);
41
162
  window.addEventListener('message', this._onMessage);
163
+ if (this.auto) {
164
+ this.systemQuery.addEventListener('change', this._onSystem);
165
+ window.addEventListener('storage', this._onStorage);
166
+ }
42
167
  }
43
168
 
44
169
  teardownEvents() {
45
170
  this._btn?.removeEventListener('click', this._onClick);
46
171
  window.removeEventListener('message', this._onMessage);
172
+ if (this._onSystem) this.systemQuery.removeEventListener('change', this._onSystem);
173
+ window.removeEventListener('storage', this._onStorage);
47
174
  }
48
175
 
49
- _syncTitle() { this._btn.title = this.getAttribute('title') || this.str('toggleTheme'); }
50
- onStringsChanged() { this._syncTitle(); }
51
-
52
176
  onAttributeChanged(name) {
53
- if (name === 'title' && this._btn) this._syncTitle();
177
+ if (!this._btn) return;
178
+ if (name === 'modes') {
179
+ // Re-arm: the listeners above hang on whether `auto` is offered.
180
+ if (this.isConnected) { this.teardownEvents(); this.setupEvents(); }
181
+ this.auto ? this._applyMode() : this._syncGlyph();
182
+ }
183
+ this._syncTitle();
54
184
  }
55
185
 
56
- /** Sets the theme on <html>, persists it and reports it. */
57
- apply(theme) {
58
- document.documentElement.setAttribute('data-theme', theme);
59
- try { localStorage.setItem(this.storageKey, theme); } catch {}
60
- this._syncGlyph();
61
- this.emit('nk-change', { value: theme });
186
+ /**
187
+ * Sets the mode on <html>, persists it and reports it. `light` / `dark`
188
+ * are stored, `auto` (only where it is offered) clears the entry – that is
189
+ * what the inline script before first paint reads.
190
+ */
191
+ apply(mode) {
192
+ const wanted = mode === 'auto' && this.auto ? 'auto' : mode === 'light' || mode === 'dark' ? mode : this.themeFor(mode);
193
+ try {
194
+ if (wanted === 'auto') localStorage.removeItem(this.storageKey);
195
+ else localStorage.setItem(this.storageKey, wanted);
196
+ this._unsaved = null;
197
+ } catch { this._unsaved = wanted; }
198
+ this._applyMode(wanted);
199
+ this.emit('nk-change', { value: this.auto ? wanted : this.themeFor(wanted), theme: this.themeFor(wanted) });
62
200
  }
63
201
 
64
202
  get storageKey() { return this.getAttribute('storage-key') || 'nk-theme'; }
65
- get value() { return getCurrentTheme(); }
203
+ /** The mode: 'auto' | 'light' | 'dark'. Without `auto` the theme, as before. */
204
+ get value() { return this.auto ? this.mode : getCurrentTheme(); }
66
205
  set value(v) { this.apply(v); }
206
+ /** The theme on screen – 'light' or 'dark', whatever the mode. */
207
+ get theme() { return this.themeFor(); }
67
208
  }
68
209
 
69
210
  customElements.define('nk-theme-toggle', NkThemeToggle);
@@ -24,6 +24,17 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
24
24
  // [data-tooltip], a press or Escape. A line break in the text is kept.
25
25
  // While it shows, the target's aria-describedby names it (in the same
26
26
  // document or shadow root).
27
+ //
28
+ // Following the pointer and the focus inside a shadow root (1.20.1):
29
+ // pointerover, focusin and focusout carry a relatedTarget, and when target
30
+ // and relatedTarget share a shadow tree the DOM standard ends the event path
31
+ // at its host – the document never hears of a move from one [data-tooltip]
32
+ // to the next in the same root. So the pointer is also read from
33
+ // pointermove, which has no relatedTarget, and acted on only when the
34
+ // element under it changes; and a key that moved the focus – Tab, the arrow
35
+ // keys of a group – is caught on keyup, comparing the focused element with
36
+ // the one before the key went down. A move by script alone inside one
37
+ // shadow root stays unseen.
27
38
  const hostSheet = new CSSStyleSheet();
28
39
  hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
29
40
  let count = 0;
@@ -101,14 +112,50 @@ class NkTooltip extends NkElement {
101
112
  this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
102
113
  }
103
114
 
115
+ /**
116
+ * The pointer is over `el` now (null: no [data-tooltip]). pointerover acts
117
+ * on every boundary it crosses, as it always has; pointermove – the only
118
+ * one that reaches the document from inside a shadow root – only when the
119
+ * element under the pointer changed, so a hand resting on the page leaves
120
+ * a tooltip the keyboard or show(rect) opened alone.
121
+ */
122
+ _pointer(e, move) {
123
+ const el = e.pointerType === 'touch' ? null : this._targetOf(e);
124
+ if (move && el === this._hover) return;
125
+ this._hover = el;
126
+ if (el === this._target) return;
127
+ this.hide();
128
+ if (!el) return;
129
+ this._target = el;
130
+ this._timer = setTimeout(() => this._showFor(el), this._delay);
131
+ }
132
+
104
133
  setupEvents() {
105
- this._onOver = (e) => {
106
- const el = e.pointerType === 'touch' ? null : this._targetOf(e);
107
- if (el === this._target) return;
134
+ this._onOver = (e) => this._pointer(e, false);
135
+ this._onMove = (e) => this._pointer(e, true);
136
+ // The pointer left the window (no relatedTarget): a tooltip – shown or
137
+ // still waiting – for what it was on goes with it. One the keyboard or
138
+ // show(rect) opened is not the pointer's and stays.
139
+ this._onOut = (e) => {
140
+ if (e.relatedTarget || e.pointerType === 'touch') return;
141
+ if (this._target && this._target === this._hover) this.hide();
142
+ this._hover = null;
143
+ };
144
+ // Capture, before an element's own keydown moves the focus (a roving tab
145
+ // stop does); not for a held key's repeats, so keyup compares with where
146
+ // the focus was before the key went down.
147
+ this._onKeyDown = (e) => { if (!e.repeat) this._focusBefore = deepActiveElement(); };
148
+ this._onKeyUp = (e) => {
149
+ const active = deepActiveElement();
150
+ if (active === this._focusBefore) return;
151
+ this._focusBefore = active;
152
+ // keyup goes to the focused element, so its path is the focus's path.
153
+ const el = this._targetOf(e);
154
+ if (el && el === this._target) return; // focusin got there first
108
155
  this.hide();
109
- if (!el) return;
156
+ if (!el || !active?.matches(':focus-visible')) return;
110
157
  this._target = el;
111
- this._timer = setTimeout(() => this._showFor(el), this._delay);
158
+ this._showFor(el);
112
159
  };
113
160
  this._onFocus = (e) => {
114
161
  const el = this._targetOf(e);
@@ -120,7 +167,11 @@ class NkTooltip extends NkElement {
120
167
  this._onHide = () => this.hide();
121
168
  this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
122
169
  document.addEventListener('pointerover', this._onOver);
170
+ document.addEventListener('pointermove', this._onMove, { capture: true, passive: true });
171
+ document.addEventListener('pointerout', this._onOut);
123
172
  document.addEventListener('focusin', this._onFocus);
173
+ document.addEventListener('keydown', this._onKeyDown, true);
174
+ document.addEventListener('keyup', this._onKeyUp, true);
124
175
  document.addEventListener('focusout', this._onHide);
125
176
  document.addEventListener('pointerdown', this._onHide, true);
126
177
  document.addEventListener('scroll', this._onHide, true);
@@ -131,13 +182,18 @@ class NkTooltip extends NkElement {
131
182
 
132
183
  teardownEvents() {
133
184
  document.removeEventListener('pointerover', this._onOver);
185
+ document.removeEventListener('pointermove', this._onMove, { capture: true });
186
+ document.removeEventListener('pointerout', this._onOut);
134
187
  document.removeEventListener('focusin', this._onFocus);
188
+ document.removeEventListener('keydown', this._onKeyDown, true);
189
+ document.removeEventListener('keyup', this._onKeyUp, true);
135
190
  document.removeEventListener('focusout', this._onHide);
136
191
  document.removeEventListener('pointerdown', this._onHide, true);
137
192
  document.removeEventListener('scroll', this._onHide, true);
138
193
  document.removeEventListener('wheel', this._onHide, { capture: true });
139
194
  document.removeEventListener('keydown', this._onKey);
140
195
  this.hide();
196
+ this._hover = this._focusBefore = null;
141
197
  }
142
198
 
143
199
  onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }