@adia-ai/web-components 0.7.26 → 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 +20 -6
- package/components/nav/nav.class.js +7 -0
- package/components/richtext/richtext.a2ui.json +5 -0
- package/components/richtext/richtext.css +20 -0
- package/components/richtext/richtext.d.ts +2 -0
- package/components/richtext/richtext.yaml +10 -0
- package/core/markdown.js +28 -3
- package/core/markdown.test.js +47 -0
- package/core/provider.js +17 -0
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +43 -43
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/README.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
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.
|
|
2
16
|
|
|
3
17
|
## [0.7.26] — 2026-07-04
|
|
4
18
|
|
|
@@ -411,7 +425,7 @@ Upstream consumer findings from `adia-pay` against installed `0.7.23` (F1–F6).
|
|
|
411
425
|
|
|
412
426
|
### Changed — `slot="caret"` disclosure-indicator convention + FEEDBACK-89 (ADR-0036)
|
|
413
427
|
|
|
414
|
-
- **`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
|
|
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`.
|
|
415
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.
|
|
416
430
|
- **`dist/` CDN bundles** (`web-components.min.css` + `web-components.min.js`) regenerated to carry the renamed `--*-caret-*` tokens + caret stamp logic.
|
|
417
431
|
|
|
@@ -3234,7 +3248,7 @@ Companion to the corpus simplification arc in `@adia-ai/a2ui-{runtime,compose,co
|
|
|
3234
3248
|
|
|
3235
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.
|
|
3236
3250
|
|
|
3237
|
-
See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative +
|
|
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.
|
|
3238
3252
|
## [0.4.3] - 2026-05-11
|
|
3239
3253
|
|
|
3240
3254
|
### Added
|
|
@@ -3250,7 +3264,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
|
|
|
3250
3264
|
|
|
3251
3265
|
### Lockstep
|
|
3252
3266
|
|
|
3253
|
-
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
|
|
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.
|
|
3254
3268
|
## [0.4.2] - 2026-05-11
|
|
3255
3269
|
|
|
3256
3270
|
### Changed
|
|
@@ -3287,7 +3301,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
|
|
|
3287
3301
|
|
|
3288
3302
|
### Lockstep
|
|
3289
3303
|
|
|
3290
|
-
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 (
|
|
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.
|
|
3291
3305
|
## [0.4.1] - 2026-05-10
|
|
3292
3306
|
|
|
3293
3307
|
### Ride-along (no source changes)
|
|
@@ -3738,7 +3752,7 @@ edge instead of leaving the column reading as dead width. Per-row
|
|
|
3738
3752
|
|
|
3739
3753
|
### Added — Trait test coverage at 100% + `<traits-host>` wrapper (2026-05-02)
|
|
3740
3754
|
|
|
3741
|
-
Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from
|
|
3755
|
+
Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from `ADR-0018`.
|
|
3742
3756
|
|
|
3743
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`).
|
|
3744
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.
|
|
@@ -3758,7 +3772,7 @@ Follow-up landing on top of the v0.2.1 trait-system work — closes the two foll
|
|
|
3758
3772
|
|
|
3759
3773
|
### Added — Trait system source-of-truth + declarative `traits=` attribute (2026-05-02)
|
|
3760
3774
|
|
|
3761
|
-
Rolls up under
|
|
3775
|
+
Rolls up under `ADR-0018`. Consumer-visible additions and one core change to UIElement's lifecycle.
|
|
3762
3776
|
|
|
3763
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.
|
|
3764
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).
|
|
@@ -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
|
-
|
|
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><script></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', {
|