@signal9/era-ui 29.0.0 → 30.0.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
@@ -1,3 +1,17 @@
1
+ ## [30.0.0](https://github.com/sig-nine/era-ui/compare/v29.0.0...v30.0.0) (2026-08-15)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * **notes:** createOverlayHost(from) now requires the element the overlay
6
+ belongs to. It is internal to the notes app, which is not published.
7
+
8
+ Claude-Session: https://claude.ai/code/session_011FjvULDrmxd61z2xUnYs6Z
9
+
10
+ ### Bug Fixes
11
+
12
+ * **apps:** an icon beside a label goes in lead, not in children ([15d0589](https://github.com/sig-nine/era-ui/commit/15d0589045929c11b4dd6a29ef4f8a570c9a0156))
13
+ * **notes:** the editor's overlay hosts carry the axes of the editor ([6a630c1](https://github.com/sig-nine/era-ui/commit/6a630c12d5847490305f279c73800e63094e5f8b))
14
+
1
15
  ## [29.0.0](https://github.com/sig-nine/era-ui/compare/v28.0.0...v29.0.0) (2026-08-15)
2
16
 
3
17
  ### ⚠ BREAKING CHANGES
@@ -0,0 +1,16 @@
1
+ export type Axes = {
2
+ /** Attributes to stamp on the receiving element. */
3
+ attrs: Record<string, string>;
4
+ /** `dark` / `light` when a scoped one is in force, else undefined. */
5
+ scheme?: 'dark' | 'light';
6
+ };
7
+ /** Read the axes governing `el`. Safe on null and on a detached node. */
8
+ export declare function axesAt(el: Element | null | undefined): Axes;
9
+ /**
10
+ * Stamp the axes governing `from` onto `to`.
11
+ *
12
+ * For hosts built by hand rather than rendered by a component, where there is
13
+ * no context to relay through and no markup to spread onto — the host has no
14
+ * other way to know where it logically belongs.
15
+ */
16
+ export declare function carryAxes(to: HTMLElement, from: Element | null | undefined): void;
@@ -0,0 +1,65 @@
1
+ import { axisKeys, axisRegistry } from './axes.js';
2
+ /**
3
+ * THE AXES IN FORCE AT AN ELEMENT — the one implementation of that question.
4
+ *
5
+ * Anything that moves content to `document.body` has to answer it, because
6
+ * everything era styles with is inherited: the density ladder and roundness
7
+ * scale are custom properties on `*`, a surface re-baselines a token contract,
8
+ * the font axis publishes an inherited stack. Content that leaves its subtree
9
+ * stops inheriting from it and starts inheriting from `:root`.
10
+ *
11
+ * AxisRelay had this logic and could not share it. It lives in a `.svelte.ts`
12
+ * module because it holds `$state`, so a plain `.ts` consumer — the notes
13
+ * editor's overlay hosts, which are created by hand rather than by a component
14
+ * — could not import it without pulling runes into a non-rune module. So the
15
+ * notes app grew its own copy, and the copy was never written: the hosts simply
16
+ * did not carry the axes, and an editor inside `data-surface="flat"` opened a
17
+ * bevelled slash menu and bubble toolbar. Reported by signalnine.org, running
18
+ * flat demos inside a bevel site.
19
+ *
20
+ * Runes-free on purpose, for exactly the reason ancestor-path.ts is: the moment
21
+ * a shared truth can only be imported by components, the non-component callers
22
+ * reimplement it or skip it.
23
+ *
24
+ * NEAREST ANCESTOR PER AXIS, INDEPENDENTLY. A subtree can override density
25
+ * without touching surface, and `closest()` answers each question separately —
26
+ * which is what makes the axes orthogonal rather than a bundle.
27
+ */
28
+ /** Theme is not an axis in the registry (no picker contract), but it inherits
29
+ * the same way and breaks across a portal the same way. */
30
+ const THEME_ATTR = 'data-theme';
31
+ /** The colour-scheme pair is a CLASS, not an attribute, so it travels apart. */
32
+ const SCHEME_CLASSES = ['dark', 'light'];
33
+ /** Read the axes governing `el`. Safe on null and on a detached node. */
34
+ export function axesAt(el) {
35
+ if (!el?.closest)
36
+ return { attrs: {} };
37
+ const attrs = {};
38
+ for (const key of axisKeys) {
39
+ const attr = axisRegistry[key].attr;
40
+ const value = el.closest(`[${attr}]`)?.getAttribute(attr);
41
+ if (value)
42
+ attrs[attr] = value;
43
+ }
44
+ const theme = el.closest(`[${THEME_ATTR}]`)?.getAttribute(THEME_ATTR);
45
+ if (theme)
46
+ attrs[THEME_ATTR] = theme;
47
+ const schemeHost = el.closest('.dark, .light');
48
+ const scheme = SCHEME_CLASSES.find((c) => schemeHost?.classList.contains(c));
49
+ return { attrs, scheme };
50
+ }
51
+ /**
52
+ * Stamp the axes governing `from` onto `to`.
53
+ *
54
+ * For hosts built by hand rather than rendered by a component, where there is
55
+ * no context to relay through and no markup to spread onto — the host has no
56
+ * other way to know where it logically belongs.
57
+ */
58
+ export function carryAxes(to, from) {
59
+ const { attrs, scheme } = axesAt(from);
60
+ for (const [attr, value] of Object.entries(attrs))
61
+ to.setAttribute(attr, value);
62
+ to.classList.remove(...SCHEME_CLASSES);
63
+ if (scheme)
64
+ to.classList.add(scheme);
65
+ }
@@ -1,5 +1,5 @@
1
1
  import { getContext, hasContext, setContext } from 'svelte';
2
- import { axisKeys, axisRegistry } from './axes.js';
2
+ import { axesAt } from './axes-at.js';
3
3
  /*
4
4
  * CARRYING THE AXES ACROSS A PORTAL.
5
5
  *
@@ -39,9 +39,6 @@ import { axisKeys, axisRegistry } from './axes.js';
39
39
  * absence is a null, not a crash.
40
40
  */
41
41
  const KEY = Symbol('era.axis-relay');
42
- /** The axis attributes, plus the theme pair, which is not in the axis registry. */
43
- const THEME_ATTR = 'data-theme';
44
- const SCHEME_CLASSES = ['dark', 'light'];
45
42
  export class AxisRelay {
46
43
  /** Attributes to stamp on portalled content. Empty until a capture runs. */
47
44
  attrs = $state({});
@@ -71,22 +68,12 @@ export class AxisRelay {
71
68
  const el = this.#anchor;
72
69
  if (!el?.closest)
73
70
  return;
74
- const next = {};
75
- for (const key of axisKeys) {
76
- const attr = axisRegistry[key].attr;
77
- const value = el.closest(`[${attr}]`)?.getAttribute(attr);
78
- if (value)
79
- next[attr] = value;
80
- }
81
- // Theme is not an axis in the registry (it has no picker contract), but it
82
- // is inherited the same way and breaks across a portal the same way.
83
- const theme = el.closest(`[${THEME_ATTR}]`)?.getAttribute(THEME_ATTR);
84
- if (theme)
85
- next[THEME_ATTR] = theme;
86
- this.attrs = next;
87
- // The colour-scheme pair is a CLASS, not an attribute, so it is separate.
88
- const schemeHost = el.closest('.dark, .light');
89
- this.scheme = SCHEME_CLASSES.find((c) => schemeHost?.classList.contains(c)) ?? undefined;
71
+ // The reading itself lives in axes-at.ts, runes-free, because the notes
72
+ // editor's hand-built overlay hosts need the same answer from a plain .ts
73
+ // module and cannot import this one. They had gone without it.
74
+ const { attrs, scheme } = axesAt(el);
75
+ this.attrs = attrs;
76
+ this.scheme = scheme;
90
77
  }
91
78
  /** The class string portalled content should carry. */
92
79
  get schemeClass() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "29.0.0",
3
+ "version": "30.0.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",