@jungherz-de/notionkit-elements 1.5.3 → 1.6.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 (103) hide show
  1. package/README.md +25 -16
  2. package/dist/components/base.js +346 -0
  3. package/dist/components/nk-ai-input-row.js +2 -1
  4. package/dist/components/nk-ai-msg.js +10 -3
  5. package/dist/components/nk-ai-thread.js +2 -1
  6. package/dist/components/nk-app.js +2 -1
  7. package/dist/components/nk-avatar-group.js +2 -1
  8. package/dist/components/nk-avatar.js +57 -0
  9. package/dist/components/nk-banner.js +2 -1
  10. package/dist/components/nk-block-host.js +2 -1
  11. package/dist/components/nk-board-view.js +4 -2
  12. package/dist/components/nk-breadcrumb.js +2 -1
  13. package/dist/components/nk-btn.js +2 -1
  14. package/dist/components/nk-callout.js +2 -1
  15. package/dist/components/nk-check.js +2 -1
  16. package/dist/components/nk-cmdk.js +2 -1
  17. package/dist/components/nk-code.js +2 -1
  18. package/dist/components/nk-comment.js +9 -6
  19. package/dist/components/nk-comments.js +2 -1
  20. package/dist/components/nk-danger-zone.js +2 -1
  21. package/dist/components/nk-database.js +2 -1
  22. package/dist/components/nk-divider.js +2 -1
  23. package/dist/components/nk-emoji-picker.js +2 -1
  24. package/dist/components/nk-empty.js +2 -1
  25. package/dist/components/nk-field.js +2 -1
  26. package/dist/components/nk-fields.js +2 -1
  27. package/dist/components/nk-filter-bar.js +2 -1
  28. package/dist/components/nk-heading.js +2 -1
  29. package/dist/components/nk-input.js +2 -1
  30. package/dist/components/nk-kbd.js +2 -1
  31. package/dist/components/nk-list-view.js +96 -0
  32. package/dist/components/nk-member-list.js +2 -1
  33. package/dist/components/nk-member-row.js +10 -6
  34. package/dist/components/nk-mention.js +2 -1
  35. package/dist/components/nk-menu-item.js +23 -3
  36. package/dist/components/nk-menu.js +2 -1
  37. package/dist/components/nk-modal.js +2 -1
  38. package/dist/components/nk-model-card.js +2 -1
  39. package/dist/components/nk-page-actions.js +2 -1
  40. package/dist/components/nk-page-cover.js +12 -11
  41. package/dist/components/nk-page-title.js +2 -1
  42. package/dist/components/nk-page.js +12 -4
  43. package/dist/components/nk-panel.js +61 -0
  44. package/dist/components/nk-panels.js +18 -0
  45. package/dist/components/nk-pop.js +2 -1
  46. package/dist/components/nk-profile-row.js +2 -1
  47. package/dist/components/nk-progress.js +5 -2
  48. package/dist/components/nk-prop.js +54 -0
  49. package/dist/components/nk-props.js +21 -0
  50. package/dist/components/nk-quote.js +2 -1
  51. package/dist/components/nk-radio.js +2 -1
  52. package/dist/components/nk-section-label.js +2 -1
  53. package/dist/components/nk-segmented.js +2 -1
  54. package/dist/components/nk-select.js +2 -1
  55. package/dist/components/nk-settings-pane.js +2 -1
  56. package/dist/components/nk-settings-user.js +2 -1
  57. package/dist/components/nk-sidebar.js +2 -1
  58. package/dist/components/nk-skeleton.js +2 -1
  59. package/dist/components/nk-slider.js +2 -1
  60. package/dist/components/nk-stat.js +2 -1
  61. package/dist/components/nk-stats.js +2 -1
  62. package/dist/components/nk-switch.js +2 -1
  63. package/dist/components/nk-synced.js +2 -1
  64. package/dist/components/nk-tab-bar-item.js +2 -1
  65. package/dist/components/nk-tab-bar.js +2 -1
  66. package/dist/components/nk-tab.js +2 -1
  67. package/dist/components/nk-table-view.js +6 -2
  68. package/dist/components/nk-tabs.js +2 -1
  69. package/dist/components/nk-tag.js +2 -1
  70. package/dist/components/nk-template-btn.js +2 -1
  71. package/dist/components/nk-textarea.js +2 -1
  72. package/dist/components/nk-theme-toggle.js +2 -1
  73. package/dist/components/nk-toast.js +2 -1
  74. package/dist/components/nk-todo.js +2 -1
  75. package/dist/components/nk-toggle.js +2 -1
  76. package/dist/components/nk-topbar.js +2 -1
  77. package/dist/components/nk-tree-item.js +2 -1
  78. package/dist/components/nk-tree.js +2 -1
  79. package/dist/components/nk-workspace-switcher.js +2 -1
  80. package/dist/components/shared/avatar-BiqCaHOt.js +19 -0
  81. package/dist/components/shared/{property-cell-CtXQpUjQ.js → property-cell-o7N30kAQ.js} +8 -3
  82. package/dist/notionkit-elements.esm.js +374 -34
  83. package/dist/notionkit-elements.js +380 -33
  84. package/dist/notionkit-elements.min.js +1 -1
  85. package/package.json +4 -3
  86. package/src/components/content/nk-progress.js +3 -1
  87. package/src/components/data/nk-ai-msg.js +8 -2
  88. package/src/components/data/nk-comment.js +7 -5
  89. package/src/components/data/nk-list-view.js +93 -0
  90. package/src/components/data/nk-table-view.js +2 -0
  91. package/src/components/overlays/nk-menu-item.js +21 -2
  92. package/src/components/page/nk-avatar.js +55 -0
  93. package/src/components/page/nk-member-row.js +8 -5
  94. package/src/components/page/nk-page-cover.js +10 -10
  95. package/src/components/page/nk-page.js +10 -3
  96. package/src/components/page/nk-panel.js +59 -0
  97. package/src/components/page/nk-panels.js +16 -0
  98. package/src/components/page/nk-prop.js +52 -0
  99. package/src/components/page/nk-props.js +19 -0
  100. package/src/index.js +14 -0
  101. package/src/util/avatar.js +17 -0
  102. package/src/util/property-cell.js +7 -3
  103. package/dist/components/shared/base-C3eJwHKA.js +0 -366
package/README.md CHANGED
@@ -1,17 +1,17 @@
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.5.3-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.6.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
- <a href="#"><img src="https://img.shields.io/badge/elements-71-529cca?style=flat-square" alt="71 Elements"></a>
6
+ <a href="#"><img src="https://img.shields.io/badge/elements-77-529cca?style=flat-square" alt="77 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.5.3-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.6.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
 
12
12
  <p align="center">
13
13
  <strong>Drop-in Web Components for <a href="https://github.com/JUNGHERZ/NotionKit">NotionKit CSS</a></strong><br>
14
- 71 vanilla JavaScript custom elements wrapping NotionKit's calm, document-centric workspace look.<br>
14
+ 77 vanilla JavaScript custom elements wrapping NotionKit's calm, document-centric workspace look.<br>
15
15
  Shadow DOM &middot; Native form participation &middot; Pixel parity with the class markup &middot; Zero dependencies.
16
16
  </p>
17
17
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  ## ✨ What is NotionKit Elements?
30
30
 
31
- NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **71 Web Components** – from buttons and form controls over the sidebar, page tree and page shell to the settings modal, ⌘K palette, database views and AI thread – that encapsulate the class markup of the foundation into simple, declarative custom elements.
31
+ NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **77 Web Components** – from buttons and form controls over the sidebar, page tree and page shell with its properties to the settings modal, ⌘K palette, database views and AI thread – that encapsulate the class markup of the foundation into simple, declarative custom elements.
32
32
 
33
33
  It is the **app layer** of the NotionKit family – three layers, one design language: [NotionKit](https://notionkit.jungherz.com) is the pure-CSS foundation, NotionKit Elements wraps it into web components for application UIs, and [NotionKit Web](https://notionkit-web.jungherz.com) is the Astro template for complete websites on top of the same foundation. The word stem stays the same across the layers: `.nk-callout` becomes `<nk-callout>`, a modifier class becomes an attribute, a state class becomes a boolean attribute.
34
34
 
@@ -56,11 +56,11 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
56
56
  |---|---|
57
57
  | 🎯 **Pixel parity** | Every element renders identically to its class markup – verified by a Playwright pixel test in light and dark; the reference app differs by 0.00 % from the class version |
58
58
  | 🔌 **Shadow DOM** | Each root adopts the NotionKit *components* sheet via `adoptedStyleSheets`; tokens are inherited from the document, so one `:root { --nk-accent: … }` re-brands everything |
59
- | 🧩 **71 Elements** | Forms, content blocks, app shell & tree, page shell, overlays (modal, ⌘K palette, menus, popover, emoji picker, toast), database table & board, comments, AI thread |
59
+ | 🧩 **77 Elements** | Forms, content blocks, app shell & tree, page shell with properties, panels and avatars, overlays (modal, ⌘K palette, menus, popover, emoji picker, toast), database table, board & list, 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** | 167 KB minified / 33 KB gzipped (IIFE, foundation sheet included), no external dependencies |
63
- | 📦 **Four bundle shapes** | IIFE, minified IIFE, ESM, and per-component ESM entries with a shared chunk |
62
+ | 🪶 **Lightweight** | 197 KB minified / 39 KB gzipped (IIFE, foundation sheet included), no external dependencies |
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
 
66
66
  ---
@@ -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.5.3/notionkit.min.css">
75
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.5.3/dist/notionkit-elements.min.js"></script>
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.6.0/notionkit.min.css">
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.6.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">
@@ -90,10 +90,19 @@ npm install @jungherz-de/notionkit-elements @jungherz-de/notionkit
90
90
  ```js
91
91
  import '@jungherz-de/notionkit/notionkit.css';
92
92
  import '@jungherz-de/notionkit-elements'; // everything
93
- import '@jungherz-de/notionkit-elements/components/nk-btn.js'; // or one element (shared code is one extra chunk)
93
+ import '@jungherz-de/notionkit-elements/components/nk-btn.js'; // or one element (shared code: base.js)
94
+ import { componentsSheet } from '@jungherz-de/notionkit-elements'; // the sheet the elements adopt, for your own views
94
95
  ```
95
96
 
96
- Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.5.1`; 1.5.1 handles the safe areas, 1.5.0 brings the Notion-2025 tokens, the nine tag colours and the filled inputs, 1.4.x segmented scroll/wrap, the fixed tab bar and the dvh shell, 1.3.x the field layouts; 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.
97
+ The per-component files import NotionKit's sheet as `@jungherz-de/notionkit/notionkit-styles.js` rather than carrying a copy, so a project that adopts the sheet in its own views has it once. A bundler resolves it from `node_modules`; a build-free page adds one import-map entry:
98
+
99
+ ```html
100
+ <script type="importmap">{ "imports": { "@jungherz-de/notionkit/notionkit-styles.js": "/node_modules/@jungherz-de/notionkit/notionkit-styles.js" } }</script>
101
+ ```
102
+
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
+
105
+ Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.6.0`; 1.6.0 brings 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.
97
106
 
98
107
  ## Elements
99
108
 
@@ -102,9 +111,9 @@ Two prerequisites, always: `notionkit.css` on the document (the peer dependency,
102
111
  | Forms & controls | `nk-btn` `nk-input` `nk-textarea` `nk-select` `nk-switch` `nk-check` `nk-radio` `nk-slider` `nk-field` `nk-fields` |
103
112
  | Content | `nk-tag` `nk-progress` `nk-callout` `nk-divider` `nk-heading` `nk-toggle` `nk-todo` `nk-kbd` `nk-code` `nk-quote` |
104
113
  | App shell & navigation | `nk-app` `nk-sidebar` `nk-workspace-switcher` `nk-section-label` `nk-tree` `nk-tree-item` `nk-topbar` `nk-breadcrumb` `nk-theme-toggle` `nk-tab-bar` `nk-tab-bar-item` |
105
- | Page shell & blocks | `nk-page` `nk-page-cover` `nk-page-title` `nk-page-actions` `nk-block-host` `nk-banner` `nk-empty` `nk-skeleton` `nk-synced` `nk-tabs` `nk-tab` `nk-segmented` `nk-stats` `nk-stat` `nk-avatar-group` `nk-mention` `nk-template-btn` `nk-model-card` `nk-profile-row` `nk-danger-zone` `nk-member-list` `nk-member-row` |
114
+ | Page shell & blocks | `nk-page` `nk-page-cover` `nk-page-title` `nk-page-actions` `nk-props` `nk-prop` `nk-panels` `nk-panel` `nk-avatar` `nk-block-host` `nk-banner` `nk-empty` `nk-skeleton` `nk-synced` `nk-tabs` `nk-tab` `nk-segmented` `nk-stats` `nk-stat` `nk-avatar-group` `nk-mention` `nk-template-btn` `nk-model-card` `nk-profile-row` `nk-danger-zone` `nk-member-list` `nk-member-row` |
106
115
  | Overlays | `nk-modal` `nk-settings-pane` `nk-settings-user` `nk-cmdk` `nk-menu` `nk-menu-item` `nk-pop` `nk-emoji-picker` `nk-toast` |
107
- | Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
116
+ | Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-list-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
108
117
 
109
118
  Static content goes in through attributes and slots, dynamic data through properties (`tree.data`, `database.rows`, `palette.commands`). Interactions fire custom events with fixed names: `nk-select`, `nk-change`, `nk-view-change`, `nk-command`, `nk-toggle`, `nk-submit`, `nk-action`. No fetching, no two-way binding – the elements render what they get.
110
119
 
@@ -114,7 +123,7 @@ Static content goes in through attributes and slots, dynamic data through proper
114
123
  - Every host is `display: contents`: the inner `.nk-*` element sits in the parent layout exactly where the class markup would. That is what makes the pixel parity hold – and why spacing belongs on a wrapper you own, not on the host.
115
124
  - Form controls are form-associated custom elements: `FormData`, `reset`, `required` and `<fieldset disabled>` just work. `<nk-radio>`s with the same `name` form a real group across shadow roots, with one tab stop and arrow keys.
116
125
  - Elements that copy light-DOM children (`nk-select` options, breadcrumb crumbs) watch them; `element.refresh()` is the escape hatch. Listeners are re-armed on every connect, so a moved element keeps working.
117
- - `::slotted()` matches only the assigned node: pass icons directly (`<span slot="icon">📁</span>`), never wrapped.
126
+ - `::slotted()` matches only the assigned node: pass icons directly (`<span slot="icon">📁</span>`), never wrapped. For the same reason there is no `<nk-prose>`: rendered Markdown goes into a light-DOM `<div class="nk-prose">`, which reads the same rules as the editor adapter.
118
127
  - Overlays (`nk-modal`, `nk-cmdk`, `nk-toast`) go directly under `<body>`; they lock scroll, make the rest of the page `inert` and return focus.
119
128
  - The editor stays an adapter: `nk-block-host` is the shell, `docs-editor.js` the TipTap recipe (used in the demo app). `<nk-editor>` follows in v1.1 as an optional import, never in the core bundle.
120
129
 
@@ -122,7 +131,7 @@ Static content goes in through attributes and slots, dynamic data through proper
122
131
 
123
132
  ```bash
124
133
  npm install
125
- npm run build # dist/: IIFE, minified IIFE, ESM, per-component ESM + shared chunk
134
+ npm run build # dist/: IIFE, minified IIFE, ESM, per-component ESM on dist/components/base.js
126
135
  npm run build:all # + docs.html, showcase.html, index.html, de/, SKILL.md, llms.txt
127
136
  npm test # Playwright: pixel parity vs. class markup (both themes), branding, reconnect, radio groups, light-DOM drift, forms, overlays, data views, per-component import
128
137
  npm run check:coverage && npm run check:versions
@@ -0,0 +1,346 @@
1
+ import { tokensCss, componentsSheet } from '@jungherz-de/notionkit/notionkit-styles.js';
2
+
3
+ // ============================================================
4
+ // NotionKit Elements – base classes
5
+ //
6
+ // Every <nk-*> element is a thin shell around the markup notionkit.css already
7
+ // styles. The shadow root adopts the *components* half of that stylesheet and
8
+ // nothing else; the element ships no visual CSS of its own.
9
+ // ============================================================
10
+
11
+ // ── Design tokens ──
12
+ // The shadow roots deliberately adopt componentsSheet only. The full sheet
13
+ // would bring the :root / [data-theme] token blocks along, and those match the
14
+ // .nk-wrapper below — every --nk-* token would then be re-declared inside each
15
+ // shadow root, where a matching rule always beats an inherited value. A page's
16
+ // own `:root { --nk-accent: … }` would never arrive.
17
+ //
18
+ // So the token defaults go on the document once, and every shadow root
19
+ // inherits them like any other custom property. They sit in a cascade layer so
20
+ // an ordinary (unlayered) brand stylesheet — and notionkit.css itself, if the
21
+ // page loads it — wins over them, no matter the load order.
22
+
23
+ const TOKENS_INJECTED = '__nkDefaultTokensInjected';
24
+
25
+ function injectDefaultTokens() {
26
+ if (typeof document === 'undefined') return; // SSR / non-DOM
27
+ if (globalThis[TOKENS_INJECTED]) return; // another bundle copy did it
28
+ globalThis[TOKENS_INJECTED] = true;
29
+
30
+ const sheet = new CSSStyleSheet();
31
+ sheet.replaceSync(`@layer notionkit-defaults { ${tokensCss} }`);
32
+ // Append — never assign — so an app's own adopted sheets survive.
33
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
34
+ }
35
+
36
+ injectDefaultTokens();
37
+
38
+ // ── Global theme sync ──
39
+ // data-theme on <html> is the only source of truth. One MutationObserver
40
+ // mirrors it onto every registered instance; no element sets a theme itself.
41
+
42
+ const instances = new Set();
43
+
44
+ function getCurrentTheme() {
45
+ return document.documentElement.getAttribute('data-theme') || 'light';
46
+ }
47
+
48
+ function syncAllThemes() {
49
+ const theme = getCurrentTheme();
50
+ for (const instance of instances) instance._syncTheme(theme);
51
+ }
52
+
53
+ if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
54
+ new MutationObserver(syncAllThemes).observe(document.documentElement, {
55
+ attributes: true,
56
+ attributeFilter: ['data-theme']
57
+ });
58
+ }
59
+
60
+ // ── Host stylesheets ──
61
+ // Layout plumbing only: how the host box participates in the outer layout, and
62
+ // that `hidden` keeps working. :host([hidden]) has a higher specificity than
63
+ // :host, so it wins regardless of order. The wrapper is layout-transparent.
64
+
65
+ const hostSheets = new Map();
66
+
67
+ function hostSheetFor(display) {
68
+ let sheet = hostSheets.get(display);
69
+ if (!sheet) {
70
+ sheet = new CSSStyleSheet();
71
+ sheet.replaceSync(`
72
+ :host { display: ${display}; }
73
+ :host([hidden]) { display: none; }
74
+ .nk-wrapper { display: contents; }
75
+ `);
76
+ hostSheets.set(display, sheet);
77
+ }
78
+ return sheet;
79
+ }
80
+
81
+ // ── Base class ──
82
+
83
+ class NkElement extends HTMLElement {
84
+
85
+ /**
86
+ * How the host takes part in the outer layout. Default 'contents': the host
87
+ * generates no box, so the inner .nk-* element sits in the parent's layout
88
+ * exactly where the class markup would – a button stays inline, a sidebar
89
+ * is a direct flex child of the app, a tree row and its children box are
90
+ * siblings. That is what makes the pixel parity with the class markup hold.
91
+ * Consequence: style the parent or the tokens, not the host; `hidden` on
92
+ * the host still works. Override with 'block' / 'inline-block' only when a
93
+ * host box is genuinely needed.
94
+ */
95
+ static get display() { return 'contents'; }
96
+
97
+ /**
98
+ * Optional CSSStyleSheet with component-specific host rules, adopted after
99
+ * the shared sheets. Positioning and sizing of the host only — never colour,
100
+ * spacing or typography; those belong to notionkit.css.
101
+ */
102
+ static get hostStyles() { return null; }
103
+
104
+ /**
105
+ * Opt in when the component *copies* light-DOM children into its shadow tree
106
+ * (options, breadcrumb crumbs). A MutationObserver then calls
107
+ * projectLightDom() again whenever those children change, so a framework
108
+ * that swaps them keeps the rendered element in step. Components that merely
109
+ * *slot* their children use `slotchange` instead.
110
+ */
111
+ static get observesLightDom() { return false; }
112
+
113
+ /** What the light-DOM observer watches. Drop `subtree` for shallow copies. */
114
+ static get lightDomObserverInit() {
115
+ return { childList: true, subtree: true, characterData: true, attributes: true };
116
+ }
117
+
118
+ /**
119
+ * 'named' (default) or 'manual'. Manual assignment lets a component route
120
+ * light-DOM nodes itself – text nodes into a label slot, nested elements
121
+ * into a children slot – so <nk-tree-item icon="🏠">Home</nk-tree-item>
122
+ * reads naturally. With 'manual' the component calls assignSlots() on
123
+ * every childList change.
124
+ */
125
+ static get slotAssignment() { return 'named'; }
126
+
127
+ static get observedAttributes() { return []; }
128
+
129
+ constructor() {
130
+ super();
131
+ this._initialized = false;
132
+ this._shadow = this.attachShadow({ mode: 'open', slotAssignment: this.constructor.slotAssignment });
133
+ const sheets = [componentsSheet, hostSheetFor(this.constructor.display)];
134
+ const extra = this.constructor.hostStyles;
135
+ if (extra) sheets.push(extra);
136
+ this._shadow.adoptedStyleSheets = sheets;
137
+ }
138
+
139
+ connectedCallback() {
140
+ if (!this._initialized) {
141
+ // A framework may have set properties on the instance before the
142
+ // definition arrived (hybrids binds checked="${bool}" as a property
143
+ // while the element is still a plain HTMLElement). Such an own property
144
+ // shadows the accessor on the prototype for good; re-apply it through
145
+ // the setter so the attribute – and with it render() – sees the value.
146
+ this._upgradeOwnProperties();
147
+ this._initialized = true;
148
+
149
+ // The wrapper mirrors data-theme from <html> so [data-theme]-keyed
150
+ // component rules apply inside the root. Token *values* need no
151
+ // mirroring — they inherit.
152
+ this._wrapper = document.createElement('div');
153
+ this._wrapper.className = 'nk-wrapper';
154
+ this._wrapper.setAttribute('data-theme', getCurrentTheme());
155
+ this._shadow.appendChild(this._wrapper);
156
+
157
+ this.render();
158
+ } else {
159
+ this._wrapper.setAttribute('data-theme', getCurrentTheme());
160
+ }
161
+
162
+ // Everything below runs on every connect, not just the first. Moving an
163
+ // element in the DOM disconnects and reconnects it, and disconnectedCallback
164
+ // tears all of this down — without re-arming it here a moved element would
165
+ // keep its markup but silently stop reacting.
166
+ this.setupEvents();
167
+ instances.add(this);
168
+
169
+ if (this.constructor.observesLightDom) {
170
+ this._lightDomObserver ??= new MutationObserver(records => this.projectLightDom(records));
171
+ this._lightDomObserver.observe(this, this.constructor.lightDomObserverInit);
172
+ }
173
+ if (this.constructor.slotAssignment === 'manual') {
174
+ this.assignSlots();
175
+ this._slotObserver ??= new MutationObserver(() => this.assignSlots());
176
+ this._slotObserver.observe(this, { childList: true });
177
+ }
178
+ }
179
+
180
+ disconnectedCallback() {
181
+ instances.delete(this);
182
+ this._lightDomObserver?.disconnect();
183
+ this._slotObserver?.disconnect();
184
+ this.teardownEvents();
185
+ }
186
+
187
+ attributeChangedCallback(name, oldValue, newValue) {
188
+ if (!this._initialized) return;
189
+ if (oldValue === newValue) return;
190
+ this.onAttributeChanged(name, oldValue, newValue);
191
+ }
192
+
193
+ _syncTheme(theme) {
194
+ this._wrapper?.setAttribute('data-theme', theme);
195
+ }
196
+
197
+ _upgradeOwnProperties() {
198
+ for (const key of Object.keys(this)) {
199
+ if (key.startsWith('_')) continue;
200
+ let proto = Object.getPrototypeOf(this), desc;
201
+ while (proto && proto !== HTMLElement.prototype && !(desc = Object.getOwnPropertyDescriptor(proto, key))) proto = Object.getPrototypeOf(proto);
202
+ if (!desc?.set) continue;
203
+ const value = this[key];
204
+ delete this[key];
205
+ this[key] = value;
206
+ }
207
+ }
208
+
209
+ /** Subclasses override to build the inner DOM inside this._wrapper. Runs once. */
210
+ render() {}
211
+
212
+ /** Subclasses override to attach event listeners. Runs on every connect. */
213
+ setupEvents() {}
214
+
215
+ /** Subclasses override to remove event listeners. Runs on every disconnect. */
216
+ teardownEvents() {}
217
+
218
+ /** Subclasses override to react to attribute changes after render(). */
219
+ onAttributeChanged(name, oldValue, newValue) {}
220
+
221
+ /**
222
+ * Subclasses that set observesLightDom override this to (re-)copy their
223
+ * light-DOM children into the shadow tree. Runs on every change to those
224
+ * children, so it has to be safe to call repeatedly.
225
+ */
226
+ projectLightDom() {}
227
+
228
+ /** Components with manual slot assignment override this. */
229
+ assignSlots() {}
230
+
231
+ /**
232
+ * Escape hatch: re-copy the light-DOM children now. The observer covers the
233
+ * ordinary cases; this is for the ones it cannot see, so nobody has to reach
234
+ * into element.shadowRoot.
235
+ */
236
+ refresh() { this.projectLightDom(); if (this.constructor.slotAssignment === 'manual') this.assignSlots(); }
237
+
238
+ // ── Utilities ──
239
+
240
+ /**
241
+ * For elements whose heading arrives as `title` – nk-settings-pane,
242
+ * nk-danger-zone, nk-empty, nk-model-card. `title` is also the global HTML
243
+ * attribute, and a title on the host shows as a browser tooltip over the
244
+ * whole element. So the value is read into this._titleText and the
245
+ * attribute is taken off the host; a later setAttribute('title') arrives
246
+ * through attributeChangedCallback and is taken the same way. The removal
247
+ * fires the callback with null, which is ignored – only '' clears the
248
+ * heading. The element's own `title` accessor answers from the stored
249
+ * value, so a framework that binds `title` as a property never puts an
250
+ * attribute on the host at all. Returns whether a value was taken.
251
+ */
252
+ takeTitle(value = this.getAttribute('title')) {
253
+ if (value === null) return false;
254
+ this._titleText = value;
255
+ this.removeAttribute('title');
256
+ return true;
257
+ }
258
+
259
+ getBoolAttr(name) {
260
+ return this.hasAttribute(name);
261
+ }
262
+
263
+ setBoolAttr(name, value) {
264
+ if (value) this.setAttribute(name, '');
265
+ else this.removeAttribute(name);
266
+ }
267
+
268
+ createElement(tag, classes = [], attrs = {}) {
269
+ const el = document.createElement(tag);
270
+ if (classes.length) el.classList.add(...classes);
271
+ for (const [key, val] of Object.entries(attrs)) {
272
+ if (val !== null && val !== undefined && val !== false) el.setAttribute(key, val === true ? '' : val);
273
+ }
274
+ return el;
275
+ }
276
+
277
+ /** Dispatches a composed, bubbling CustomEvent — the nk-* event contract. */
278
+ emit(eventName, detail = null) {
279
+ return this.dispatchEvent(new CustomEvent(eventName, {
280
+ bubbles: true,
281
+ composed: true,
282
+ cancelable: true,
283
+ detail
284
+ }));
285
+ }
286
+ }
287
+
288
+ // ── Form-associated base class ──
289
+ // ElementInternals gives the element a place in the surrounding <form>:
290
+ // FormData, reset, validation and :disabled through <fieldset>.
291
+
292
+ class NkFormElement extends NkElement {
293
+
294
+ static formAssociated = true;
295
+
296
+ constructor() {
297
+ super();
298
+ this._internals = this.attachInternals();
299
+ }
300
+
301
+ get form() { return this._internals.form; }
302
+ get validationMessage() { return this._internals.validationMessage; }
303
+ get validity() { return this._internals.validity; }
304
+ get willValidate() { return this._internals.willValidate; }
305
+
306
+ checkValidity() { return this._internals.checkValidity(); }
307
+ reportValidity() { return this._internals.reportValidity(); }
308
+
309
+ connectedCallback() {
310
+ const first = !this._initialized;
311
+ super.connectedCallback();
312
+ // A `disabled` attribute in the markup fires formDisabledCallback on
313
+ // parse or upgrade – before the first connect, when nothing is rendered
314
+ // yet. The state was remembered below; apply it now that render() ran.
315
+ if (first && this._formDisabled) this.onFormDisabled(true);
316
+ }
317
+
318
+ formResetCallback() { this.resetValue(); }
319
+ formStateRestoreCallback(state, mode) { this.restoreValue(state, mode); }
320
+ formDisabledCallback(disabled) {
321
+ this._formDisabled = disabled;
322
+ if (this._initialized) this.onFormDisabled(disabled);
323
+ }
324
+
325
+ /** Subclasses override. */
326
+ resetValue() {}
327
+ restoreValue(state, mode) {}
328
+ onFormDisabled(disabled) {}
329
+
330
+ setFormValue(value, state) {
331
+ this._internals.setFormValue(value, state);
332
+ }
333
+
334
+ setValidity(flags, message, anchor) {
335
+ this._internals.setValidity(flags, message, anchor);
336
+ }
337
+
338
+ /** Mirrors the native control's validity onto the host, so required works. */
339
+ syncValidityFrom(control) {
340
+ if (!control) return;
341
+ if (control.validity.valid) this._internals.setValidity({});
342
+ else this._internals.setValidity(control.validity, control.validationMessage, control);
343
+ }
344
+ }
345
+
346
+ export { NkElement, NkFormElement, getCurrentTheme };
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-ai-input-row placeholder="Ask Mona something …"></nk-ai-input-row>
4
5
  // → <div class="nk-ai-input-row"><span>✨</span><input><button class="nk-ai-send">↑</button></div>
@@ -1,20 +1,23 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-ai-msg role="user" name="You" avatar="MK">Summarise the open tasks.</nk-ai-msg>
4
5
  // <nk-ai-msg role="assistant" name="Mona" badge="· AI" avatar="✨">
5
6
  // Two tasks are open …
6
7
  // <button slot="actions" value="copy">📋 Copy</button><button slot="actions" value="retry">↻ Rephrase</button>
7
8
  // </nk-ai-msg>
9
+ // <nk-ai-msg role="user" bubble>What is still open?</nk-ai-msg> → a grey bubble on the right, as in Notion's AI chat
8
10
  // → <div class="nk-ai-msg user"><span class="mini-avatar">MK</span><div class="a-body"><div class="a-name">You</div>…<div class="nk-ai-actions">…</div></div></div>
9
11
  // Action buttons stay in the light DOM (slotted twins) and fire nk-action { action: value }.
10
12
  class NkAiMsg extends NkElement {
11
- static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color']; }
13
+ static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color', 'bubble']; }
12
14
 
13
15
  render() {
14
16
  this._box = this.createElement('div', ['nk-ai-msg']);
15
17
  this._avatar = this.createElement('span', ['mini-avatar']);
16
18
  const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
17
19
  avatarSlot.appendChild(this._avatar);
20
+ this._avatarSlot = avatarSlot;
18
21
  const body = this.createElement('div', ['a-body']);
19
22
  this._name = this.createElement('div', ['a-name']);
20
23
  this._nameText = document.createTextNode('');
@@ -31,12 +34,16 @@ class NkAiMsg extends NkElement {
31
34
 
32
35
  _sync() {
33
36
  const user = this.getAttribute('role') === 'user';
37
+ const bubble = this.getBoolAttr('bubble');
34
38
  this._box.classList.toggle('user', user);
39
+ this._box.classList.toggle('bubble', bubble);
35
40
  const name = this.getAttribute('name');
36
41
  this._nameText.data = name || '';
37
42
  const badge = this.getAttribute('badge');
38
43
  this._badge.textContent = badge ? ` ${badge}` : '';
39
- this._name.style.display = name ? '' : 'none';
44
+ // A bubble stands without avatar and name; the text says who asked.
45
+ this._name.style.display = name && !bubble ? '' : 'none';
46
+ this._avatarSlot.style.display = bubble ? 'none' : '';
40
47
  this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
41
48
  const color = this.getAttribute('color');
42
49
  this._avatar.style.background = color || '';
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-ai-thread><nk-ai-msg …>…</nk-ai-msg>…</nk-ai-thread> → <div class="nk-ai-thread">
4
5
  class NkAiThread extends NkElement {
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-app>
4
5
  // <nk-sidebar slot="sidebar">…</nk-sidebar>
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-avatar-group more="+2">
4
5
  // <span class="mini-avatar" style="background:#448361">SL</span>…
@@ -0,0 +1,57 @@
1
+ import { NkElement } from './base.js';
2
+ import { p as paintAvatar, i as initialsOf } from './shared/avatar-BiqCaHOt.js';
3
+ import '@jungherz-de/notionkit/notionkit-styles.js';
4
+
5
+ // <nk-avatar>MK</nk-avatar> → <span class="nk-avatar">MK</span>
6
+ // <nk-avatar size="small" color="green">SL</nk-avatar>
7
+ // <nk-avatar name="Ada Lovelace" size="large"></nk-avatar> → initials "AL"
8
+ // <nk-avatar src="ada.jpg" name="Ada Lovelace" size="xlarge"></nk-avatar>
9
+ // <nk-avatar square>A</nk-avatar> → a workspace icon
10
+ // size: small (20px) · default 24px · large (32px) · xlarge (56px). color: one
11
+ // of Notion's nine names, or any CSS background. Without content the initials
12
+ // come from `name`; with `src` a photo fills the circle and `name` is its alt.
13
+ class NkAvatar extends NkElement {
14
+ static get observedAttributes() { return ['size', 'color', 'square', 'src', 'name']; }
15
+
16
+ render() {
17
+ this._box = this.createElement('span', ['nk-avatar']);
18
+ this._slot = document.createElement('slot');
19
+ this._initials = document.createTextNode('');
20
+ this._slot.appendChild(this._initials);
21
+ this._img = this.createElement('img', [], { alt: '' });
22
+ this._box.appendChild(this._slot);
23
+ this._wrapper.appendChild(this._box);
24
+ this._sync();
25
+ }
26
+
27
+ _sync() {
28
+ const size = this.getAttribute('size');
29
+ for (const s of ['small', 'large', 'xlarge']) this._box.classList.toggle(s, s === size);
30
+ this._box.classList.toggle('square', this.getBoolAttr('square'));
31
+ paintAvatar(this._box, this.getAttribute('color'));
32
+ const name = this.getAttribute('name');
33
+ this._initials.data = initialsOf(name);
34
+ const src = this.getAttribute('src');
35
+ if (src) {
36
+ this._img.src = src;
37
+ this._img.alt = name || '';
38
+ if (this._slot.isConnected) this._slot.replaceWith(this._img);
39
+ } else if (this._img.isConnected) this._img.replaceWith(this._slot);
40
+ // A picture or initials with a name speak for themselves; a bare glyph is decoration.
41
+ if (name && !src) { this._box.setAttribute('role', 'img'); this._box.setAttribute('aria-label', name); }
42
+ else { this._box.removeAttribute('role'); this._box.removeAttribute('aria-label'); }
43
+ }
44
+
45
+ onAttributeChanged() { this._sync(); }
46
+
47
+ get size() { return this.getAttribute('size'); }
48
+ set size(v) { v == null ? this.removeAttribute('size') : this.setAttribute('size', v); }
49
+ get color() { return this.getAttribute('color'); }
50
+ set color(v) { v == null ? this.removeAttribute('color') : this.setAttribute('color', v); }
51
+ get src() { return this.getAttribute('src'); }
52
+ set src(v) { v == null ? this.removeAttribute('src') : this.setAttribute('src', v); }
53
+ }
54
+
55
+ customElements.define('nk-avatar', NkAvatar);
56
+
57
+ export { NkAvatar };
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-banner variant="info">ℹ️ <span>Text</span><span slot="action">Open</span></nk-banner>
4
5
  // → <div class="nk-banner info">…<span class="b-action">Open</span></div>
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-block-host handle><p>Editable content lives here.</p></nk-block-host>
4
5
  // → <div class="nk-block-host"><span class="nk-block-handle">⠿</span>…</div>
@@ -1,5 +1,7 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
2
- import { t as tagFor, r as renderPropertyCell } from './shared/property-cell-CtXQpUjQ.js';
1
+ import { NkElement } from './base.js';
2
+ import { t as tagFor, r as renderPropertyCell } from './shared/property-cell-o7N30kAQ.js';
3
+ import '@jungherz-de/notionkit/notionkit-styles.js';
4
+ import './shared/avatar-BiqCaHOt.js';
3
5
 
4
6
  // <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
5
7
  // Groups rows by a select column (default: the first select column); one
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-breadcrumb>
4
5
  // <a href="/overview">📊 Project overview</a>
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-btn variant="primary" small>Save</nk-btn> → <button class="nk-btn primary small">
4
5
  // <nk-btn href="/docs" variant="secondary">Docs</nk-btn> → <a class="nk-btn secondary">
@@ -1,4 +1,5 @@
1
- import { N as NkElement } from './shared/base-C3eJwHKA.js';
1
+ import { NkElement } from './base.js';
2
+ import '@jungherz-de/notionkit/notionkit-styles.js';
2
3
 
3
4
  // <nk-callout icon="💡">text</nk-callout>
4
5
  // <nk-callout><span slot="icon">📌</span>text</nk-callout>