@adia-ai/web-components 0.7.25 → 0.7.27

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,4 +1,23 @@
1
1
  # Changelog — @adia-ai/web-components
2
+ ## [0.7.27] — 2026-07-12
3
+
4
+ Fixes harvested from the /site → A2UI docs migration's live operator reports (TKT-0002…0006) — each was a real framework defect the migration surfaced, not a docs-only issue — plus the router race that three separate flake classes traced back to.
5
+
6
+ ### Added
7
+ - **`richtext-ui` (`components/richtext/`) — `flush` boolean prop: paragraph-grade embedding mode.** Removes the article chrome (the `[data-richtext-body]` `space-8`/`space-6` padding, the 45rem reading measure, the component's own canvas background, and the trailing paragraph margin) so rendered markdown sits flush in a host column like a plain text block. Use it when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave it off for standalone articles and chat transcripts, which keep the article treatment unchanged. Motivation: the docs transpiler rides richtext for every paragraph with inline code marks — 1,672 instances across 275 pages each carried article insets (TKT-0004 B).
8
+
9
+ ### Fixed
10
+ - **`router-ui` (`core/provider.js`) — `#loadContent` claims a monotonic navigation token BEFORE any await.** A navigation that started earlier but fetched its content slowly could resume after a faster follow-up navigation had fully rendered, and — because nothing sequenced completion against navigation order — legitimately clobber the newer route's content while `data-route-path` still named the new route. Reproduced deterministically (2s-delayed boot fetch: old code showed `path=button` with the Dashboard DOM). The token is re-checked after every await (content fetch, templateResolver, setup import); a superseded call neither writes `innerHTML` nor emits `route-loaded`. This was the root mechanism behind the site-a2ui migration's "no-artifact despite a valid artifact" residue and its batch-sweep flakes.
11
+ - **`nav-ui` (`components/nav/`) — collapsed-mode flyouts no longer re-open after selecting an item (TKT-0005).** The flyout popover is appended inside its `nav-group-ui`, so an option click bubbled to `nav-ui`'s delegated click handler, whose collapsed branch called `group.showPopover()` for any click landing inside the group — instantly re-opening the flyout the option's own handler had just closed. The collapsed branch now ignores clicks originating inside `[popover]`, mirroring the header guard the expanded branch always had.
12
+ - **`core/markdown.js` — inline code spans survive emphasis/link regexes.** `inline()` now extracts backtick code spans into placeholders before applying the bold/italic/link transforms and restores them verbatim after — code spans containing `*`, `_`, `[`, or `]` no longer render mangled. New unit suite (`core/markdown.test.js`, 6 cases).
13
+
14
+ ### Maintenance
15
+ - `dist/` CDN bundles regenerated (`web-components.min.css`, `web-components.min.js`, `theme-provider.min.js`) carrying the richtext `flush` rules + the router/nav/markdown fixes.
16
+
17
+ ## [0.7.26] — 2026-07-04
18
+
19
+ ### Fixed
20
+ - **`empty-state-ui` (`components/empty-state/`) — the AI-composition rule no longer recommends `variant="outline"` for secondary/retry actions.** The `a2ui: rules:` guidance in `components/empty-state/empty-state.yaml` said "Prefer `[variant="primary"]` for create-flows; `[variant="outline"]` for retry / secondary actions" — reinforcing a repo-wide habit of reaching for `outline` just to mark a button as non-primary, when the unstyled default already IS the secondary look (`:where(:scope)` base tokens read `--a-ui-bg`, distinct from `variant="primary"`'s `--a-primary` accent fill). Corrected to: `variant="primary"` for the create-flow CTA, leave `variant` unset for retry/secondary. Regenerated `catalog-a2ui_0_9_rules.txt` from the yaml SoT (never hand-edited); mirrored the same correction into `components/empty-state/empty-state.examples.html`'s doc list. Root-cause investigation (corpus evidence: 446 `button-ui` tags harvested, only 30 bare vs. 91 explicit `outline` + 215 `ghost`) + the companion fixes in `@adia-ai/a2ui-compose` are in that package's changelog.
2
21
 
3
22
  ## [0.7.25] — 2026-07-03
4
23
 
@@ -406,7 +425,7 @@ Upstream consumer findings from `adia-pay` against installed `0.7.23` (F1–F6).
406
425
 
407
426
  ### Changed — `slot="caret"` disclosure-indicator convention + FEEDBACK-89 (ADR-0036)
408
427
 
409
- - **`components/tree/` (`tree-item-ui`) + `components/pane/` (`pane-ui`)** — renamed the expand/collapse indicator slot `slot="chevron"` → **`slot="caret"`** (and tokens `--*-chevron-*` → `--*-caret-*`, e.g. `--tree-caret-size`, `--pane-caret-fg`), standardizing on the slot name already used by `accordion-item-ui` / `nav-group-ui` / `select-ui` and matching the Phosphor `caret-*` icon vocabulary. **No backward-compat alias** (hard rename; the caret is auto-stamped, so blast radius is small). See [ADR-0036](../../.brain/adrs/0036-caret-slot-disclosure-convention.md).
428
+ - **`components/tree/` (`tree-item-ui`) + `components/pane/` (`pane-ui`)** — renamed the expand/collapse indicator slot `slot="chevron"` → **`slot="caret"`** (and tokens `--*-chevron-*` → `--*-caret-*`, e.g. `--tree-caret-size`, `--pane-caret-fg`), standardizing on the slot name already used by `accordion-item-ui` / `nav-group-ui` / `select-ui` and matching the Phosphor `caret-*` icon vocabulary. **No backward-compat alias** (hard rename; the caret is auto-stamped, so blast radius is small). See `ADR-0036`.
410
429
  - **`tree-item-ui` adopt-or-stamp (FEEDBACK-89)** — `#stamp()` now adopts a consumer's declarative `slot="caret"` and `slot="actions"` children into the auto-stamped row instead of leaving them as siblings below it. A `<button-ui slot="actions">` (the natural way to add a per-row action — add / rename / overflow) now lands inline in the styled, hover-revealed actions area. The host `#onClick` already excludes `[slot="actions"] *` from row selection, so adoption is click-safe. +3 unit tests.
411
430
  - **`dist/` CDN bundles** (`web-components.min.css` + `web-components.min.js`) regenerated to carry the renamed `--*-caret-*` tokens + caret stamp logic.
412
431
 
@@ -3229,7 +3248,7 @@ Companion to the corpus simplification arc in `@adia-ai/a2ui-{runtime,compose,co
3229
3248
 
3230
3249
  Catalog (`packages/a2ui/corpus/catalog-a2ui_0_9.json`) regenerated to pick up every yaml addition. `verify:traits` clean at 56/56; `components --verify` clean at 125 files.
3231
3250
 
3232
- See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative + [.claude/docs/journal/2026/05/2026-05-12.md](../../.claude/docs/journal/2026/05/2026-05-12.md) §§ 43 / 44 / 50 / 51 for per-§ details.
3251
+ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative + `.claude/docs/journal/2026/05/2026-05-12.md` §§ 43 / 44 / 50 / 51 for per-§ details.
3233
3252
  ## [0.4.3] - 2026-05-11
3234
3253
 
3235
3254
  ### Added
@@ -3245,7 +3264,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
3245
3264
 
3246
3265
  ### Lockstep
3247
3266
 
3248
- 9-package coordinated PATCH cut to v0.4.3 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). Ratifies [ADR-0027](../../.brain/adrs/0027-cross-primitive-composition-imports.md) — cross-primitive composition imports are the consumer's responsibility; the primitive's `.js` does NOT side-effect-import composed primitives. Source files touched: `components/input/input.{js,css,yaml,a2ui.json}`. See root [CHANGELOG.md `## [0.4.3]`](../../CHANGELOG.md) for the cut narrative.
3267
+ 9-package coordinated PATCH cut to v0.4.3 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). Ratifies `ADR-0027` — cross-primitive composition imports are the consumer's responsibility; the primitive's `.js` does NOT side-effect-import composed primitives. Source files touched: `components/input/input.{js,css,yaml,a2ui.json}`. See root [CHANGELOG.md `## [0.4.3]`](../../CHANGELOG.md) for the cut narrative.
3249
3268
  ## [0.4.2] - 2026-05-11
3250
3269
 
3251
3270
  ### Changed
@@ -3282,7 +3301,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
3282
3301
 
3283
3302
  ### Lockstep
3284
3303
 
3285
- 9-package coordinated PATCH cut to v0.4.2 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). ADR-0025 ([`.brain/adrs/0025-no-native-form-controls.md`](../../.brain/adrs/0025-no-native-form-controls.md)) ratified — codifies the "no native or suppressed-native interactive widgets in primitives" principle that the `<input-ui type="number">` rewrite implements; `password` remains the documented exception (physically requires `<input type="password">` for `-webkit-text-security`). See root [CHANGELOG.md `## [0.4.2]`](../../CHANGELOG.md) for the cut narrative.
3304
+ 9-package coordinated PATCH cut to v0.4.2 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). ADR-0025 (`.brain/adrs/0025-no-native-form-controls.md`) ratified — codifies the "no native or suppressed-native interactive widgets in primitives" principle that the `<input-ui type="number">` rewrite implements; `password` remains the documented exception (physically requires `<input type="password">` for `-webkit-text-security`). See root [CHANGELOG.md `## [0.4.2]`](../../CHANGELOG.md) for the cut narrative.
3286
3305
  ## [0.4.1] - 2026-05-10
3287
3306
 
3288
3307
  ### Ride-along (no source changes)
@@ -3733,7 +3752,7 @@ edge instead of leaving the column reading as dead width. Per-row
3733
3752
 
3734
3753
  ### Added — Trait test coverage at 100% + `<traits-host>` wrapper (2026-05-02)
3735
3754
 
3736
- Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from [ADR-0018](../../.brain/adrs/0018-trait-source-of-truth-and-declarative-attribute.md).
3755
+ Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from `ADR-0018`.
3737
3756
 
3738
3757
  - **31 new per-trait behavior test files.** Every trait in `traits/` now has a sibling `.test.js`. Total trait-suite footprint: **44 files / 169 tests** (was 12 / 68). Coverage: **42/42 = 100%** (41 trait files + `traits-host`).
3739
3758
  - **`verify:traits` CI gate flipped from soft-warning to hard-fail.** `npm run verify:traits` exits non-zero if any trait file lacks a sibling `.test.js`, with the offending names listed in the failure message. Verified via mutation test: deleting `active-state.test.js` immediately fails the gate; restoring it returns to clean.
@@ -3753,7 +3772,7 @@ Follow-up landing on top of the v0.2.1 trait-system work — closes the two foll
3753
3772
 
3754
3773
  ### Added — Trait system source-of-truth + declarative `traits=` attribute (2026-05-02)
3755
3774
 
3756
- Rolls up under [ADR-0018](../../.brain/adrs/0018-trait-source-of-truth-and-declarative-attribute.md). Consumer-visible additions and one core change to UIElement's lifecycle.
3775
+ Rolls up under `ADR-0018`. Consumer-visible additions and one core change to UIElement's lifecycle.
3757
3776
 
3758
3777
  - **`defineTrait()` schema extended.** Now requires `category` (one of nine canonical values, frozen in `define.js`) and `description` (≥ 11 chars). All 40 trait files updated to declare both inline. Build-time throw on missing fields.
3759
3778
  - **New `getTrait(name)` export from `traits/define.js`.** Returns the trait factory for a kebab-case name, or null if not yet imported. `getTraitSchema(name)` now reads from the same registry (was a separate map).
@@ -116,9 +116,11 @@ a2ui:
116
116
  magnifying-glass).
117
117
  - >-
118
118
  Put AT MOST ONE CTA in slot="action". It should be the single
119
- next step (Create, Retry, Clear filters). Prefer
120
- [variant="primary"] for create-flows; [variant="outline"] for
121
- retry / secondary actions.
119
+ next step (Create, Retry, Clear filters). Use [variant="primary"]
120
+ for a create-flow CTA; leave [variant] unset for retry / secondary
121
+ actions the default (no attribute) already IS the secondary
122
+ look, so don't add [variant="outline"] just to mark something as
123
+ non-primary.
122
124
  - >-
123
125
  Use [minimal] for inline empty rows inside <table-ui> /
124
126
  <list-ui> where full-canvas chrome is too prominent. The
@@ -121,6 +121,13 @@ export class UINav extends UIElement {
121
121
  if (group && this.contains(group)) {
122
122
  const isCollapsed = this.collapsed || this.getBoundingClientRect().width <= 96;
123
123
  if (isCollapsed) {
124
+ // The flyout popover is appended INSIDE the group, so an option
125
+ // click bubbles here after the option's own handler has selected
126
+ // and hidePopover()'d — re-showing would instantly reopen the
127
+ // flyout the user just dismissed (TKT-0005, 2026-07-12). The
128
+ // expanded branch below guards on the header; this is the
129
+ // collapsed-branch equivalent.
130
+ if (e.target.closest('[popover]')) return;
124
131
  group.showPopover?.();
125
132
  } else {
126
133
  const header = group.querySelector(':scope > [slot="header"]');
@@ -16,6 +16,11 @@
16
16
  "component": {
17
17
  "const": "RichText"
18
18
  },
19
+ "flush": {
20
+ "description": "Paragraph-grade embedding mode — removes the article chrome (body padding and the 45rem reading measure, plus the trailing paragraph margin) so the rendered markdown sits flush in a host column like a plain text block. Use when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave off for standalone articles and chat transcripts.",
21
+ "type": "boolean",
22
+ "default": false
23
+ },
19
24
  "markdown": {
20
25
  "description": "Component property: markdown.",
21
26
  "type": "string",
@@ -98,6 +98,26 @@
98
98
  padding: var(--richtext-py) var(--richtext-px);
99
99
  }
100
100
 
101
+ /* [flush] — paragraph-grade embedding (yaml `flush` prop): drop the
102
+ article chrome (padding, 45rem measure, own canvas background) so the
103
+ markdown sits in a host column like a plain text block. The trailing
104
+ child keeps no bottom margin — rhythm belongs to the host layout's
105
+ gap, matching text-ui paragraphs in the doc column (TKT-0004: every
106
+ code-bearing doc paragraph rides richtext). */
107
+ :scope[flush] {
108
+ background: transparent;
109
+ overflow-y: visible;
110
+ }
111
+
112
+ :scope[flush] [data-richtext-body] {
113
+ max-width: none;
114
+ padding: 0;
115
+ }
116
+
117
+ :scope[flush] [data-richtext-body] > :last-child {
118
+ margin-bottom: 0;
119
+ }
120
+
101
121
  /* ── Headings ── */
102
122
 
103
123
  [data-richtext-body] h1 {
@@ -21,6 +21,8 @@ editable>.
21
21
  import { UIElement } from '../../core/element.js';
22
22
 
23
23
  export class UIRichText extends UIElement {
24
+ /** Paragraph-grade embedding mode — removes the article chrome (body padding and the 45rem reading measure, plus the trailing paragraph margin) so the rendered markdown sits flush in a host column like a plain text block. Use when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave off for standalone articles and chat transcripts. */
25
+ flush: boolean;
24
26
  /** Component property: markdown. */
25
27
  markdown: string;
26
28
  /** Source URL (image, iframe, etc.). */
@@ -23,6 +23,16 @@ props:
23
23
  description: Source URL (image, iframe, etc.).
24
24
  type: string
25
25
  default: ''
26
+ flush:
27
+ description: >-
28
+ Paragraph-grade embedding mode — removes the article chrome (body
29
+ padding and the 45rem reading measure, plus the trailing paragraph
30
+ margin) so the rendered markdown sits flush in a host column like a
31
+ plain text block. Use when richtext carries a single paragraph or
32
+ fragment inside an already-measured layout (doc pages, cards); leave
33
+ off for standalone articles and chat transcripts.
34
+ type: boolean
35
+ default: false
26
36
  events: {}
27
37
  slots:
28
38
  default:
package/core/markdown.js CHANGED
@@ -86,16 +86,41 @@ export function renderMarkdown(src) {
86
86
  return out.join('\n');
87
87
  }
88
88
 
89
+ // Placeholder marker for protected code-span content — \0 can't occur in
90
+ // authored markdown, so it round-trips unambiguously through the regex pass.
91
+ const CODE_PH = (n) => `\0CODE${n}\0`;
92
+ const CODE_PH_RE = /\0CODE(\d+)\0/g;
93
+
89
94
  function inline(text) {
90
- return esc(text)
95
+ const escaped = esc(text);
96
+
97
+ // 1. Extract inline-code spans FIRST and protect their (already-escaped,
98
+ // HTML-safe) raw content behind a placeholder — otherwise a code span
99
+ // containing `*`/`_` (e.g. `--a-chrome-*`, `foo_bar`) gets its content
100
+ // mangled by the emphasis regexes below before the code regex ever
101
+ // sees it (root cause of the inline-flow gap — see
102
+ // site-a2ui/FINDINGS.md "No inline-flow construct").
103
+ const codeSpans = [];
104
+ let out = escaped.replace(/`([^`]+)`/g, (_, code) => {
105
+ const i = codeSpans.length;
106
+ codeSpans.push(code);
107
+ return CODE_PH(i);
108
+ });
109
+
110
+ // 2. Now it's safe to apply emphasis + links to the remaining (non-code)
111
+ // text — code-span content is inert placeholder text at this point.
112
+ out = out
91
113
  // Bold **text** or __text__
92
114
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
93
115
  .replace(/__(.+?)__/g, '<strong>$1</strong>')
94
116
  // Italic *text* or _text_
95
117
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
96
118
  .replace(/_(.+?)_/g, '<em>$1</em>')
97
- // Inline code `code`
98
- .replace(/`([^`]+)`/g, '<code>$1</code>')
99
119
  // Links [text](url)
100
120
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
121
+
122
+ // 3. Restore the protected code spans verbatim.
123
+ out = out.replace(CODE_PH_RE, (_, i) => `<code>${codeSpans[Number(i)]}</code>`);
124
+
125
+ return out;
101
126
  }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderMarkdown } from './markdown.js';
3
+
4
+ // ── inline() protect-verbatim-regions regression (site-a2ui/FINDINGS.md
5
+ // "No inline-flow construct" — root cause: emphasis regexes ran BEFORE
6
+ // code-span extraction, so any code span containing `*`/`_` mangled) ──
7
+
8
+ describe('renderMarkdown — inline code spans survive markdown-unsafe content', () => {
9
+ it('code span containing an asterisk is not treated as emphasis', () => {
10
+ const html = renderMarkdown('Use `--a-chrome-*` for chrome tokens.');
11
+ expect(html).toContain('<code>--a-chrome-*</code>');
12
+ expect(html).not.toContain('<em>');
13
+ expect(html).not.toContain('<strong>');
14
+ });
15
+
16
+ it('code span containing underscores is not treated as emphasis', () => {
17
+ const html = renderMarkdown('Call `foo_bar_baz()` to run it.');
18
+ expect(html).toContain('<code>foo_bar_baz()</code>');
19
+ expect(html).not.toContain('<em>');
20
+ });
21
+
22
+ it('code span containing double-asterisks is not treated as bold', () => {
23
+ const html = renderMarkdown('The pattern `**not-bold**` is literal.');
24
+ expect(html).toContain('<code>**not-bold**</code>');
25
+ expect(html).not.toContain('<strong>');
26
+ });
27
+
28
+ it('bold, italic, code, and link combine correctly within one run', () => {
29
+ const html = renderMarkdown('This is **bold**, this is *italic*, this is `code_with_*` and this is [a link](https://example.com).');
30
+ expect(html).toContain('<strong>bold</strong>');
31
+ expect(html).toContain('<em>italic</em>');
32
+ expect(html).toContain('<code>code_with_*</code>');
33
+ expect(html).toContain('<a href="https://example.com">a link</a>');
34
+ });
35
+
36
+ it('adjacent code spans do not bleed into each other', () => {
37
+ const html = renderMarkdown('First `a_b` then `c*d` then plain _italic_ text.');
38
+ expect(html).toContain('<code>a_b</code>');
39
+ expect(html).toContain('<code>c*d</code>');
40
+ expect(html).toContain('<em>italic</em>');
41
+ });
42
+
43
+ it('HTML-unsafe characters inside a code span are still escaped', () => {
44
+ const html = renderMarkdown('Use `<script>` tags carefully.');
45
+ expect(html).toContain('<code>&lt;script&gt;</code>');
46
+ });
47
+ });
package/core/provider.js CHANGED
@@ -165,6 +165,19 @@ export class UIRouter extends UIProvider {
165
165
  #cache = new Map();
166
166
  #ownCtrl = null;
167
167
  #lastPath = null;
168
+ // Monotonic navigation token — claimed at #loadContent ENTRY, before any
169
+ // await, so completion order can never impersonate navigation order. A
170
+ // call that awaits (content fetch, templateResolver) and finds a newer
171
+ // token on resume is superseded: it must neither write innerHTML nor
172
+ // emit route-loaded. Incident (2026-07-12, prod deploy verify run
173
+ // 29199534931): the boot route's SLOW content fetch resumed AFTER a
174
+ // fast follow-up navigation had fully rendered — downstream guards that
175
+ // sequence at templateResolver entry saw the late resumer as "newest"
176
+ // and let it clobber the newer route's content (data-route-path said
177
+ // the new route; the DOM showed the old one). Same class as the
178
+ // "no-artifact despite a valid artifact" and batch-sweep flakes in
179
+ // site-a2ui/FINDINGS.md — the cancellation belongs HERE, at the source.
180
+ #navSeq = 0;
168
181
 
169
182
  get #ctrl() { return this.#ownCtrl || this.controller; }
170
183
 
@@ -214,6 +227,7 @@ export class UIRouter extends UIProvider {
214
227
  }
215
228
 
216
229
  async #loadContent(route, ctrl) {
230
+ const nav = ++this.#navSeq; // claim BEFORE any await — see #navSeq
217
231
  let html;
218
232
  if (this.#cache.has(route.content)) {
219
233
  html = this.#cache.get(route.content);
@@ -228,10 +242,12 @@ export class UIRouter extends UIProvider {
228
242
  return;
229
243
  }
230
244
  }
245
+ if (nav !== this.#navSeq) return; // superseded while fetching content
231
246
 
232
247
  // Template resolver hook — lets the site wrap content in a template before injection
233
248
  if (this.templateResolver) {
234
249
  html = await this.templateResolver(html, route);
250
+ if (nav !== this.#navSeq) return; // superseded while resolving
235
251
  }
236
252
 
237
253
  this.innerHTML = html;
@@ -267,6 +283,7 @@ export class UIRouter extends UIProvider {
267
283
  if (mod.default) mod.default(this);
268
284
  } catch { /* setup module is optional */ }
269
285
  }
286
+ if (nav !== this.#navSeq) return; // superseded during setup import — no phantom route-loaded
270
287
 
271
288
  const state = ctrl.getState();
272
289
  this.emit('route-loaded', {