@jungherz-de/notionkit-elements 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ 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.0] – 2026-10-12
8
+
9
+ Built against NotionKit CSS 1.20.0 (peer `>= 1.20.0`).
10
+
11
+ ### Added
12
+ - **`<nk-theme-toggle modes="auto light dark">` – a third state that follows
13
+ the system.** `auto` is what no stored value means, so the documented
14
+ inline script before first paint keeps working and a choice made without
15
+ `modes` still counts; while it is in force the element follows
16
+ `prefers-color-scheme` live, which the toggle never did. A click goes from
17
+ `auto` to the theme *not* on screen, then to the other one, then back, so
18
+ every click but the last changes something visibly. The glyph shows the
19
+ mode – `.nk-theme-glyph`, sun and moon diagonally for `auto` – instead of
20
+ the next theme, and the title names the mode and the next step (four new
21
+ texts: `appearance`, `themeAuto`, `themeLight`, `themeDark`). A choice made
22
+ in another tab arrives through the `storage` event. `value` returns
23
+ `auto | light | dark`, the new `theme` what is on screen, `nk-change`
24
+ reports both; `nextMode()` and `themeFor(mode)` are public.
25
+ **Two states stay the default**, where nothing about this element changes.
26
+ Requested by Mailverse (#2).
27
+ - `theme-color` keeps `<meta name="theme-color">` in step: while a theme is
28
+ fixed every bar carries that theme's colour, back on `auto` each one
29
+ carries its own again. The colours are the page's, never invented.
30
+
31
+ ### Fixed
32
+ - The toggle added its system-preference listener to a throwaway
33
+ `matchMedia()` object and removed it from a second one, so it was never
34
+ released and could be collected with its query. One query per element now.
35
+ - A mode whose write failed (Safari's private mode refuses to store but
36
+ reads) is kept in the element until the page reloads, instead of falling
37
+ back to `auto` on the next read.
38
+
7
39
  ## [1.19.0] – 2026-09-25
8
40
 
9
41
  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 +778,7 @@ built and tested against NotionKit CSS 1.1.1 (peer range `>= 1.0.0`).
746
778
  import, never in the core bundle – shadow-less, adding `nk-block-host` to
747
779
  itself so the foundation's editor adapter rules apply.
748
780
 
781
+ [1.20.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.0
749
782
  [1.19.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.19.0
750
783
  [1.18.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.18.0
751
784
  [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.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-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.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
 
@@ -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.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">
@@ -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.0) 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.0/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 -->
@@ -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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.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 |
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.0 · 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);