@jungherz-de/notionkit-elements 1.6.0 → 1.7.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.
@@ -0,0 +1,59 @@
1
+ import { NkElement } from '../../base.js';
2
+
3
+ // <nk-steps label="Connect your own model" current="2"
4
+ // steps="Choose a provider, Enter the API key, Test the connection"></nk-steps>
5
+ // steps.steps = [{ label: 'Choose a provider', desc: 'Anthropic' }, 'Enter the API key', …];
6
+ // → <ol class="nk-steps" aria-label="…">
7
+ // <li class="nk-step done"><span class="st-mark">✓</span><span>Choose a provider<span class="st-desc">Anthropic</span></span></li>
8
+ // <li class="nk-step current" aria-current="step"><span class="st-mark">2</span><span>Enter the API key</span></li>
9
+ // <li class="nk-step"><span class="st-mark">3</span><span>Test the connection</span></li></ol>
10
+ // A setup in a few steps (NotionKit 1.7.0). `current` counts from 1; the
11
+ // steps before it are done, one past the last marks all of them done. The
12
+ // `steps` attribute is a comma-separated list; the property also takes
13
+ // { label, desc } objects for a line under the label.
14
+ class NkSteps extends NkElement {
15
+ static get observedAttributes() { return ['steps', 'current', 'label']; }
16
+
17
+ render() {
18
+ this._list = this.createElement('ol', ['nk-steps']);
19
+ this._wrapper.appendChild(this._list);
20
+ this._sync();
21
+ }
22
+
23
+ _items() {
24
+ if (this._steps) return this._steps;
25
+ return (this.getAttribute('steps') || '').split(',').map(s => s.trim()).filter(Boolean);
26
+ }
27
+
28
+ _sync() {
29
+ const label = this.getAttribute('label');
30
+ if (label) this._list.setAttribute('aria-label', label); else this._list.removeAttribute('aria-label');
31
+ const current = this.current;
32
+ this._list.replaceChildren(...this._items().map((step, i) => {
33
+ const n = i + 1, { label: text, desc } = typeof step === 'object' && step ? step : { label: String(step) };
34
+ const li = this.createElement('li', ['nk-step']);
35
+ li.classList.toggle('done', n < current);
36
+ li.classList.toggle('current', n === current);
37
+ if (n === current) li.setAttribute('aria-current', 'step');
38
+ const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
39
+ mark.textContent = n < current ? '✓' : String(n);
40
+ const body = document.createElement('span');
41
+ body.textContent = text ?? '';
42
+ if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
43
+ li.append(mark, body);
44
+ return li;
45
+ }));
46
+ }
47
+
48
+ onAttributeChanged() { this._sync(); }
49
+
50
+ get steps() { return this._items(); }
51
+ set steps(v) { this._steps = Array.isArray(v) ? v : null; if (this._list) this._sync(); }
52
+ get current() { const n = parseInt(this.getAttribute('current'), 10); return Number.isFinite(n) ? n : 1; }
53
+ set current(v) { this.setAttribute('current', String(v)); }
54
+ /** Moves on to the next step; returns the new current step. */
55
+ next() { this.current = Math.min(this.current + 1, this._items().length + 1); return this.current; }
56
+ }
57
+
58
+ customElements.define('nk-steps', NkSteps);
59
+ export { NkSteps };
@@ -10,42 +10,12 @@ import { NkElement } from '../../base.js';
10
10
  //
11
11
  // The host is display:contents so the <aside> is a direct flex child of
12
12
  // .nk-app, exactly like the class markup. Below 860px notionkit.css hides the
13
- // aside; `open` brings it back as an off-canvas drawer (position only – the
14
- // look is still the stylesheet's).
15
- //
16
- // The drawer slides and the scrim fades without a line of JavaScript:
17
- // `display` transitions with transition-behavior: allow-discrete (the old
18
- // value is held for the whole duration, so closing animates too) and
19
- // @starting-style gives the first frame after display: none its start
20
- // values. `position: fixed` lives on the plain .nk-sidebar, not on the open
21
- // state – while closing, the aside is still displayed and must not fall
22
- // back into the flow. Browsers without either feature switch hard, as
23
- // before; prefers-reduced-motion switches hard on purpose.
24
- const drawerSheet = new CSSStyleSheet();
25
- drawerSheet.replaceSync(`
26
- .nk-sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 59; background: var(--nk-scrim-soft); }
27
- @media (max-width: 860px) {
28
- .nk-sidebar {
29
- position: fixed; inset: 0 auto 0 0; z-index: 60;
30
- transform: translateX(-100%);
31
- transition: background .25s ease, transform .24s cubic-bezier(.2, .8, .25, 1), display .24s;
32
- transition-behavior: allow-discrete;
33
- }
34
- :host([open]) .nk-sidebar { display: flex; transform: none; }
35
- @starting-style { :host([open]) .nk-sidebar { transform: translateX(-100%); } }
36
- .nk-sidebar-backdrop {
37
- opacity: 0;
38
- transition: opacity .24s ease, display .24s;
39
- transition-behavior: allow-discrete;
40
- }
41
- :host([open]) .nk-sidebar-backdrop { display: block; opacity: 1; }
42
- @starting-style { :host([open]) .nk-sidebar-backdrop { opacity: 0; } }
43
- }
44
- @media (prefers-reduced-motion: reduce) { .nk-sidebar, .nk-sidebar-backdrop { transition: none; } }
45
- `);
46
-
13
+ // aside; `open` brings it back as a drawer over the page. The drawer is the
14
+ // stylesheet's since NotionKit 1.7.0 (.nk-sidebar.open, .nk-sidebar-backdrop
15
+ // .open) – the sidebar slides and the scrim fades by the same rules as the
16
+ // class markup; the element toggles the two classes. <nk-btn variant="sidebar">
17
+ // in the topbar is the ☰ that opens it.
47
18
  class NkSidebar extends NkElement {
48
- static get hostStyles() { return drawerSheet; }
49
19
  static get observedAttributes() { return ['open']; }
50
20
 
51
21
  render() {
@@ -62,6 +32,13 @@ class NkSidebar extends NkElement {
62
32
  this._wrapper.appendChild(this._backdrop);
63
33
  this._wrapper.appendChild(this._aside);
64
34
  this._syncFooter();
35
+ this._syncOpen();
36
+ }
37
+
38
+ _syncOpen() {
39
+ const open = this.getBoolAttr('open');
40
+ this._aside.classList.toggle('open', open);
41
+ this._backdrop.classList.toggle('open', open);
65
42
  }
66
43
 
67
44
  _syncFooter() {
@@ -88,7 +65,9 @@ class NkSidebar extends NkElement {
88
65
  }
89
66
 
90
67
  onAttributeChanged(name) {
91
- if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open') });
68
+ if (name !== 'open') return;
69
+ this._syncOpen();
70
+ this.emit('nk-toggle', { open: this.getBoolAttr('open') });
92
71
  }
93
72
 
94
73
  show() { this.setBoolAttr('open', true); }
package/src/index.js CHANGED
@@ -80,6 +80,10 @@ export { NkProp } from './components/page/nk-prop.js';
80
80
  export { NkPanels } from './components/page/nk-panels.js';
81
81
  export { NkPanel } from './components/page/nk-panel.js';
82
82
 
83
+ // 1.7.0 – mobile and filter
84
+ export { NkSheet } from './components/overlays/nk-sheet.js';
85
+ export { NkSteps } from './components/page/nk-steps.js';
86
+
83
87
  // Wave 4 – overlays
84
88
  export { NkModal } from './components/overlays/nk-modal.js';
85
89
  export { NkSettingsPane } from './components/overlays/nk-settings-pane.js';
package/src/util/focus.js CHANGED
@@ -1,6 +1,7 @@
1
- // Overlay plumbing shared by nk-modal and nk-cmdk: body scroll lock with a
2
- // refcount, and `inert` on everything outside the overlay so focus and
3
- // assistive technology stay inside – nested shadow roots included.
1
+ // Overlay plumbing shared by nk-modal, nk-sheet, nk-cmdk and nk-menu: body
2
+ // scroll lock with a refcount, `inert` on everything outside the overlay so
3
+ // focus and assistive technology stay inside – nested shadow roots included –
4
+ // and the focused element behind shadow hosts, to return focus to.
4
5
  let locks = 0, previousOverflow = '';
5
6
 
6
7
  export function lockScroll() {
@@ -31,6 +32,13 @@ export function inertOutside(el) {
31
32
  return () => { for (const c of made) c.inert = false; };
32
33
  }
33
34
 
35
+ /** The focused element itself, looking into shadow roots (document.activeElement stops at the host). */
36
+ export function deepActiveElement() {
37
+ let el = document.activeElement;
38
+ while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
39
+ return el;
40
+ }
41
+
34
42
  /** The first tabbable element inside a light-DOM subtree, looking into shadow roots. */
35
43
  export function firstFocusable(root) {
36
44
  const SEL = 'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"], [contenteditable="plaintext-only"]';