@signal9/era-ui 30.2.1 → 30.2.2

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
@@ -1,3 +1,9 @@
1
+ ## [30.2.2](https://github.com/sig-nine/era-ui/compare/v30.2.1...v30.2.2) (2026-08-16)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **provider:** portalled content keeps its axes with no trigger rendered ([ed15a44](https://github.com/sig-nine/era-ui/commit/ed15a445eccafa582907e4bf08d9735edd9140cf))
6
+
1
7
  ## [30.2.1](https://github.com/sig-nine/era-ui/compare/v30.2.0...v30.2.1) (2026-08-16)
2
8
 
3
9
  ### Bug Fixes
@@ -23,8 +23,11 @@
23
23
 
24
24
  // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
25
  // it, so a mount-time read runs once at page load and never again.
26
+ // activeElement is the FALLBACK anchor, used only when no Trigger rendered —
27
+ // see AxisRelay.refresh(). It is read here, at the instant `open` turns true,
28
+ // because bits-ui moves focus into the panel a moment later.
26
29
  $effect(() => {
27
- if (open) relay.refresh();
30
+ if (open) relay.refresh(document.activeElement);
28
31
  });
29
32
  </script>
30
33
 
@@ -23,8 +23,11 @@
23
23
 
24
24
  // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
25
  // it, so a mount-time read runs once at page load and never again.
26
+ // activeElement is the FALLBACK anchor, used only when no Trigger rendered —
27
+ // see AxisRelay.refresh(). It is read here, at the instant `open` turns true,
28
+ // because bits-ui moves focus into the panel a moment later.
26
29
  $effect(() => {
27
- if (open) relay.refresh();
30
+ if (open) relay.refresh(document.activeElement);
28
31
  });
29
32
  </script>
30
33
 
@@ -32,9 +32,15 @@
32
32
  * before a consumer has scoped anything, and never again. Reading when `open`
33
33
  * turns true is the moment the answer is actually needed, and it re-reads on
34
34
  * every open, so an axis that changed in between is picked up.
35
+ *
36
+ * `document.activeElement` is the FALLBACK anchor, used only when no Trigger
37
+ * rendered — see AxisRelay.refresh() for why that case exists and what it
38
+ * costs. It is read HERE, at the instant `open` turns true, because bits-ui
39
+ * moves focus into the panel a moment later and the answer would be the
40
+ * panel itself.
35
41
  */
36
42
  $effect(() => {
37
- if (open) relay.refresh();
43
+ if (open) relay.refresh(document.activeElement);
38
44
  });
39
45
  </script>
40
46
 
@@ -23,8 +23,11 @@
23
23
 
24
24
  // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
25
  // it, so a mount-time read runs once at page load and never again.
26
+ // activeElement is the FALLBACK anchor, used only when no Trigger rendered —
27
+ // see AxisRelay.refresh(). It is read here, at the instant `open` turns true,
28
+ // because bits-ui moves focus into the panel a moment later.
26
29
  $effect(() => {
27
- if (open) relay.refresh();
30
+ if (open) relay.refresh(document.activeElement);
28
31
  });
29
32
  </script>
30
33
 
@@ -23,8 +23,11 @@
23
23
 
24
24
  // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
25
  // it, so a mount-time read runs once at page load and never again.
26
+ // activeElement is the FALLBACK anchor, used only when no Trigger rendered —
27
+ // see AxisRelay.refresh(). It is read here, at the instant `open` turns true,
28
+ // because bits-ui moves focus into the panel a moment later.
26
29
  $effect(() => {
27
- if (open) relay.refresh();
30
+ if (open) relay.refresh(document.activeElement);
28
31
  });
29
32
  </script>
30
33
 
@@ -25,7 +25,7 @@
25
25
  relay.register(ref);
26
26
  });
27
27
  $effect(() => {
28
- if (value) relay.refresh();
28
+ if (value) relay.refresh(document.activeElement);
29
29
  });
30
30
  </script>
31
31
 
@@ -20,7 +20,7 @@ export declare class AxisRelay {
20
20
  * without touching surface, and closest() answers each question separately,
21
21
  * which is what makes the axes orthogonal in the first place.
22
22
  */
23
- refresh(): void;
23
+ refresh(fallback?: Element | null): void;
24
24
  /** The class string portalled content should carry. */
25
25
  get schemeClass(): string;
26
26
  }
@@ -64,8 +64,34 @@ export class AxisRelay {
64
64
  * without touching surface, and closest() answers each question separately,
65
65
  * which is what makes the axes orthogonal in the first place.
66
66
  */
67
- refresh() {
68
- const el = this.#anchor;
67
+ refresh(fallback) {
68
+ /*
69
+ * NO TRIGGER, NO ANCHOR — the gap this fallback closes.
70
+ *
71
+ * A dialog driven purely by `bind:open` renders no Trigger, so there is
72
+ * no node left in the subtree to read from and the content inherited
73
+ * from :root. That is not an edge case: driving a confirm dialog from
74
+ * state is the ordinary way to write one, and it is why a consumer went
75
+ * on restating `data-font` on every Dialog.Content long after the relay
76
+ * was supposed to have made that unnecessary. Measured before this: a
77
+ * dialog opened from a button inside a `data-font="serif"` section came
78
+ * up with data-font null and font-family monospace.
79
+ *
80
+ * The fallback is WHERE THE USER WAS when it opened — document.
81
+ * activeElement, passed in by the Root at the instant `open` turns
82
+ * true, before bits-ui moves focus into the panel. A state-driven
83
+ * dialog is nearly always opened by something the user just clicked,
84
+ * and that something sits exactly where the dialog logically belongs.
85
+ *
86
+ * <body> is not an anchor: with nothing focused there is nothing to
87
+ * learn, and reading from it would just re-derive :root the long way.
88
+ * A dialog opened by a timer while focus is elsewhere therefore keeps
89
+ * the old behaviour rather than picking up an unrelated subtree.
90
+ */
91
+ const el = this.#anchor ??
92
+ (fallback && fallback !== document.body && fallback !== document.documentElement
93
+ ? fallback
94
+ : null);
69
95
  if (!el?.closest)
70
96
  return;
71
97
  // The reading itself lives in axes-at.ts, runes-free, because the notes
@@ -23,8 +23,11 @@
23
23
 
24
24
  // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
25
  // it, so a mount-time read runs once at page load and never again.
26
+ // activeElement is the FALLBACK anchor, used only when no Trigger rendered —
27
+ // see AxisRelay.refresh(). It is read here, at the instant `open` turns true,
28
+ // because bits-ui moves focus into the panel a moment later.
26
29
  $effect(() => {
27
- if (open) relay.refresh();
30
+ if (open) relay.refresh(document.activeElement);
28
31
  });
29
32
  </script>
30
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "30.2.1",
3
+ "version": "30.2.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",