@adia-ai/web-components 0.8.31 → 0.8.32

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/components/accordion/accordion.d.ts +2 -2
  3. package/components/action-list/action-list.class.js +6 -2
  4. package/components/action-list/action-list.d.ts +1 -1
  5. package/components/agent-artifact/agent-artifact.d.ts +1 -1
  6. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +2 -2
  7. package/components/agent-questions/agent-questions.d.ts +1 -1
  8. package/components/agent-reasoning/agent-reasoning.d.ts +3 -3
  9. package/components/agent-suggestions/agent-suggestions.d.ts +1 -1
  10. package/components/agent-trace/agent-trace.d.ts +1 -1
  11. package/components/alert/alert.d.ts +2 -2
  12. package/components/anchor-bar/anchor-bar.d.ts +2 -2
  13. package/components/avatar/avatar.class.js +0 -1
  14. package/components/avatar/avatar.css +16 -2
  15. package/components/badge/badge.css +30 -2
  16. package/components/breadcrumb/breadcrumb.css +15 -3
  17. package/components/button/button.d.ts +1 -1
  18. package/components/calendar-grid/calendar-grid.d.ts +2 -2
  19. package/components/card/card.css +21 -0
  20. package/components/card/card.yaml +9 -0
  21. package/components/chart/chart.d.ts +4 -4
  22. package/components/chart-legend/chart-legend.d.ts +1 -1
  23. package/components/chat-thread/chat-input.css +1 -1
  24. package/components/chat-thread/chat-thread.d.ts +2 -2
  25. package/components/check/check.css +3 -3
  26. package/components/command/command.d.ts +2 -2
  27. package/components/context-menu/context-menu.d.ts +3 -3
  28. package/components/datetime-picker/datetime-picker.d.ts +5 -5
  29. package/components/demo-toggle/demo-toggle.d.ts +1 -1
  30. package/components/drawer/drawer.d.ts +2 -2
  31. package/components/heatmap/heatmap.d.ts +5 -5
  32. package/components/index.js +1 -0
  33. package/components/inline-edit/inline-edit.d.ts +4 -4
  34. package/components/input/input.a2ui.json +4 -1
  35. package/components/input/input.css +18 -0
  36. package/components/input/input.yaml +11 -1
  37. package/components/integration-card/integration-card.d.ts +4 -4
  38. package/components/link/link.d.ts +1 -1
  39. package/components/list/list.class.js +22 -7
  40. package/components/list/list.d.ts +1 -1
  41. package/components/list-window/list-window.d.ts +5 -5
  42. package/components/menu/menu.css +2 -2
  43. package/components/menu/menu.d.ts +1 -1
  44. package/components/modal/modal.d.ts +1 -1
  45. package/components/nav/nav.css +8 -0
  46. package/components/nav/nav.d.ts +1 -1
  47. package/components/nav-group/nav-group.css +58 -9
  48. package/components/nav-group/nav-group.d.ts +1 -1
  49. package/components/nav-item/nav-item.css +7 -4
  50. package/components/nav-item/nav-item.d.ts +1 -1
  51. package/components/noodles/noodles.d.ts +3 -3
  52. package/components/option-card/option-card.css +1 -1
  53. package/components/pagination/pagination.d.ts +1 -1
  54. package/components/pane/pane.d.ts +1 -1
  55. package/components/password-strength/password-strength.d.ts +1 -1
  56. package/components/radio/radio.a2ui.json +4 -3
  57. package/components/radio/radio.css +2 -2
  58. package/components/radio/radio.yaml +7 -14
  59. package/components/radio-group/radio-group.a2ui.json +118 -0
  60. package/components/radio-group/radio-group.class.js +75 -0
  61. package/components/radio-group/radio-group.css +31 -0
  62. package/components/radio-group/radio-group.d.ts +20 -0
  63. package/components/radio-group/radio-group.examples.md +37 -0
  64. package/components/radio-group/radio-group.js +17 -0
  65. package/components/radio-group/radio-group.yaml +114 -0
  66. package/components/stat/stat.css +12 -8
  67. package/components/stat/stat.js +66 -1
  68. package/components/stat/stat.yaml +3 -0
  69. package/components/stepper/stepper-item.a2ui.json +5 -0
  70. package/components/stepper/stepper-item.yaml +11 -0
  71. package/components/stepper/stepper.a2ui.json +38 -3
  72. package/components/stepper/stepper.class.js +199 -2
  73. package/components/stepper/stepper.css +30 -0
  74. package/components/stepper/stepper.d.ts +28 -1
  75. package/components/stepper/stepper.yaml +76 -2
  76. package/components/stream/stream.d.ts +3 -3
  77. package/components/swatch/swatch.d.ts +1 -1
  78. package/components/swiper/swiper.d.ts +3 -3
  79. package/components/table/table.a2ui.json +2 -2
  80. package/components/table/table.d.ts +7 -7
  81. package/components/table/table.yaml +9 -1
  82. package/components/table-toolbar/table-toolbar.d.ts +4 -4
  83. package/components/tabs/tabs.d.ts +1 -1
  84. package/components/tag/tag.a2ui.json +1 -1
  85. package/components/tag/tag.css +78 -13
  86. package/components/tag/tag.d.ts +12 -8
  87. package/components/tag/tag.examples.md +4 -4
  88. package/components/tag/tag.yaml +18 -7
  89. package/components/tags-input/tags-input.d.ts +4 -4
  90. package/components/timeline/timeline.d.ts +1 -1
  91. package/components/toc/toc.d.ts +1 -1
  92. package/components/toggle-group/toggle-group.d.ts +1 -1
  93. package/components/toggle-scheme/toggle-scheme.d.ts +1 -1
  94. package/components/tour/tour.d.ts +4 -4
  95. package/components/tree/tree.d.ts +1 -1
  96. package/core/icons-phosphor.js +129 -71
  97. package/core/provider.js +84 -23
  98. package/dist/host.min.css +1 -1
  99. package/dist/host.sheet.js +1 -1
  100. package/dist/theme-provider.min.js +1 -1
  101. package/dist/web-components.min.css +1 -1
  102. package/dist/web-components.min.js +89 -87
  103. package/dist/web-components.sheet.js +1 -1
  104. package/package.json +1 -1
  105. package/styles/api/sizing-fallback.css +32 -0
  106. package/styles/api/sizing.css +7 -3
  107. package/styles/components.css +1 -0
  108. package/styles/tokens.css +1 -0
@@ -37,36 +37,48 @@
37
37
  * });
38
38
  *
39
39
  * Brace-list globs are statically analyzed; only the named files are
40
- * emitted as chunks (or inlined when `eager: true`). Use the `../node_modules/...`
41
- * form shown here, not `/node_modules/...` see "Why the glob is entry-relative,
42
- * not root-relative" below; the same reasoning applies to a hand-written
43
- * scoped glob as to the auto-discovery globs this file installs by default.
40
+ * emitted as chunks (or inlined when `eager: true`). Better yet, in your OWN
41
+ * source you can skip globs entirely and use plain bare-specifier `?raw`
42
+ * imports (see `icons.js`'s JSDoc, loading path 3) module resolution finds
43
+ * the package under any layout. If you do hand-write a glob, note it resolves
44
+ * by literal path join from YOUR file, not module resolution — see "Why there
45
+ * are three candidate globs" below.
44
46
  *
45
- * ## Why the glob is entry-relative, not root-relative
47
+ * ## Why there are three candidate globs, not one (gh#287 · gh#340 · gh#786)
46
48
  *
47
49
  * `import.meta.glob` only accepts a small set of static forms (`/`-rooted,
48
50
  * `./`, `../`, `**\/`, or a `#`-subpath with a literal `*`), and each resolves
49
51
  * via a literal path join, not module resolution (confirmed against Vite 8's
50
- * `toAbsoluteGlob`, gh#287). A `/`-rooted glob joins to Vite's CONFIGURED
51
- * PROJECT ROOT for a consumer app, that only reaches `@phosphor-icons/core`
52
- * when it's ALSO hoisted to that exact root. Under pnpm's default
53
- * (non-hoisted, symlinked) linking, a package you depend on only
54
- * TRANSITIVELY `@phosphor-icons/core` is a dependency of THIS package, not
55
- * necessarily your app's — is linked only inside that package's own
56
- * resolution scope, never flattened into your app's top-level `node_modules`.
57
- * A root-rooted glob then silently matches zero files.
58
- *
59
- * A `../`-prefixed glob instead joins to THIS FILE'S OWN directory
60
- * (`core/`), one level inside `@adia-ai/web-components` so `../node_modules`
61
- * reaches `@adia-ai/web-components`'s own local `node_modules`, which pnpm
62
- * always populates with a package's own direct dependencies (a symlink into
63
- * the pnpm store) regardless of whether your app also hoists them. This
64
- * holds for the published package too `core/icons-phosphor.js` ships at
65
- * the same one-level depth (see `package.json`'s `files` field). Verified
66
- * empirically against a real hoisting-gap install (a pnpm workspace where
67
- * `@phosphor-icons/core` is declared only by a nested package, not the
68
- * workspace root): the root-rooted form matches zero files; the
69
- * entry-relative form matches the full set with real content.
52
+ * `toAbsoluteGlob`, gh#287). No single literal path exists that reaches
53
+ * `@phosphor-icons/core` under every package manager's `node_modules` layout,
54
+ * so this file probes THREE entry-relative candidates (resolved from this
55
+ * file's own directory, `core/`, one level inside `@adia-ai/web-components`
56
+ * the published package ships at the same depth) and installs the first that
57
+ * matches anything:
58
+ *
59
+ * 1. `../node_modules/@phosphor-icons/core/…` pnpm's nested layout.
60
+ * pnpm always populates a package's OWN local `node_modules` with its
61
+ * direct dependencies (a symlink into the store), regardless of what the
62
+ * consumer app hoists. gh#287's original fix; still first preference.
63
+ * 2. `../../../@phosphor-icons/core/…` npm / yarn-classic hoisting in a
64
+ * consumer install. `core/` sits at
65
+ * `node_modules/@adia-ai/web-components/core/`, so three levels up IS
66
+ * the consumer's (or workspace root's) `node_modules`, where hoisting
67
+ * flattens transitive deps as siblings. This is the layout gh#786's
68
+ * npm-workspace consumer has candidate 1 is structurally guaranteed
69
+ * to match zero files there.
70
+ * 3. `../../../node_modules/@phosphor-icons/core/…` THIS repo's own
71
+ * checkout when `node_modules` is npm-shaped (gh#340: `npm ci` /
72
+ * `npm install` re-shape it). `core/` sits at
73
+ * `packages/web-components/core/`, so three levels up is the repo root
74
+ * and the hoisted copy lives in its `node_modules/`.
75
+ *
76
+ * A `/`-rooted glob is NOT among the candidates: it joins to Vite's
77
+ * CONFIGURED PROJECT ROOT, which only reaches `@phosphor-icons/core` when
78
+ * it happens to be hoisted to that exact root — the original gh#287 failure.
79
+ * Globs that match zero files are free: `import.meta.glob` returns `{}`
80
+ * without touching the filesystem at runtime, so the losing candidates cost
81
+ * nothing.
70
82
  *
71
83
  * ## Vite dep-optimizer pre-bundling (gh#287, second independent failure mode)
72
84
  *
@@ -104,19 +116,50 @@
104
116
  * to the manifest fetch below
105
117
  */
106
118
 
107
- import { installIconLoaders } from './icons.js';
119
+ import { installIconLoaders, iconRegistryUnwired } from './icons.js';
108
120
 
109
- let weightModules;
121
+ let candidates;
110
122
  let hasViteGlob = false;
111
123
  try {
112
- weightModules = {
113
- regular: import.meta.glob('../node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
114
- thin: import.meta.glob('../node_modules/@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
115
- light: import.meta.glob('../node_modules/@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
116
- bold: import.meta.glob('../node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
117
- fill: import.meta.glob('../node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
118
- duotone: import.meta.glob('../node_modules/@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
119
- };
124
+ // Three node_modules layouts, probed in order — see the JSDoc section
125
+ // "Why there are three candidate globs" above. Every glob string and its
126
+ // options object must stay a static literal (Vite's macro constraint), so
127
+ // the 18 calls can't be generated from a loop.
128
+ candidates = [
129
+ {
130
+ layout: 'pnpm-nested `../node_modules/@phosphor-icons/core`',
131
+ weights: {
132
+ regular: import.meta.glob('../node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
133
+ thin: import.meta.glob('../node_modules/@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
134
+ light: import.meta.glob('../node_modules/@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
135
+ bold: import.meta.glob('../node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
136
+ fill: import.meta.glob('../node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
137
+ duotone: import.meta.glob('../node_modules/@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
138
+ },
139
+ },
140
+ {
141
+ layout: 'npm/yarn-hoisted sibling `../../../@phosphor-icons/core`',
142
+ weights: {
143
+ regular: import.meta.glob('../../../@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
144
+ thin: import.meta.glob('../../../@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
145
+ light: import.meta.glob('../../../@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
146
+ bold: import.meta.glob('../../../@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
147
+ fill: import.meta.glob('../../../@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
148
+ duotone: import.meta.glob('../../../@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
149
+ },
150
+ },
151
+ {
152
+ layout: 'workspace-root `../../../node_modules/@phosphor-icons/core` (npm-shaped monorepo checkout)',
153
+ weights: {
154
+ regular: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
155
+ thin: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
156
+ light: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
157
+ bold: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
158
+ fill: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
159
+ duotone: import.meta.glob('../../../node_modules/@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
160
+ },
161
+ },
162
+ ];
120
163
  hasViteGlob = true;
121
164
  } catch (err) {
122
165
  // FB-08: distinguish "no Vite at all" (expected, ReferenceError) from
@@ -135,45 +178,60 @@ try {
135
178
  }
136
179
 
137
180
  if (hasViteGlob) {
138
- // FB-08: detect the silent-empty-glob case `import.meta.glob` returns
139
- // `{}` when zero files match (it does NOT throw). The previous behavior
140
- // installed empty per-weight maps and the consumer saw an empty icon
141
- // registry with no diagnostic. gh#287 names two independent causes, both
142
- // producing this same empty-map symptom:
143
- // 1. A packaging/bundler step re-nested this file at a different depth
144
- // than it ships at (breaks the `../`-relative glob's one-level-inside
145
- // assumption — see "Why the glob is entry-relative" above), a Yarn
146
- // Berry PnP install (virtual/zip filesystem), or @phosphor-icons/core
147
- // missing entirely.
148
- // 2. Vite's dev-server `optimizeDeps` pre-bundled this package (see
149
- // "Vite dep-optimizer pre-bundling" above) — confirmed empirically to
150
- // transform the glob into a literal empty object, no exception.
151
- const totalIcons = Object.values(weightModules).reduce(
181
+ // Install the first candidate layout whose globs matched anything.
182
+ // `import.meta.glob` returns `{}` when zero files match (it does NOT
183
+ // throw), so the losing layouts cost nothing.
184
+ const candidateTotal = (c) => Object.values(c.weights).reduce(
152
185
  (sum, weight) => sum + Object.keys(weight).length, 0
153
186
  );
154
- if (totalIcons === 0) {
155
- console.warn(
156
- `[icons-phosphor] No Phosphor icons matched the auto-install glob ` +
157
- `\`../node_modules/@phosphor-icons/core/assets/<weight>/*.svg\` (resolved ` +
158
- `relative to this file's own directory hoisting-safe under pnpm, gh#287). ` +
159
- `Two known causes: (1) this package got pre-bundled by Vite's dev-server ` +
160
- `optimizeDeps, which empties the glob add it to optimizeDeps.exclude in ` +
161
- `your Vite config:\n` +
162
- ` optimizeDeps: { exclude: ['@adia-ai/web-components', '@adia-ai/web-modules'] }\n` +
163
- `(2) @phosphor-icons/core is missing, or this file was re-nested to a ` +
164
- `different depth by a packaging step (Yarn PnP, a custom bundler). If ` +
165
- `neither fix applies, install loaders manually instead:\n` +
166
- ` import { installIconLoaders } from '@adia-ai/web-components/core/icons';\n` +
167
- ` import caretRight from '@phosphor-icons/core/assets/regular/caret-right.svg?raw';\n` +
168
- ` installIconLoaders({\n` +
169
- ` regular: {\n` +
170
- ` '/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': caretRight,\n` +
171
- ` },\n` +
172
- ` });\n` +
173
- `See the JSDoc header of icons-phosphor.js for the full recipe.`
174
- );
187
+ const matched = candidates.find(c => candidateTotal(c) > 0);
188
+ if (matched) {
189
+ installIconLoaders(matched.weights);
190
+ } else {
191
+ // Every candidate matched zero files. Two possibilities remain, and
192
+ // only one is a failure:
193
+ // - The consumer registers icons manually (installIconLoaders /
194
+ // registerIcon) and never wanted the auto-install path — gh#786's
195
+ // reporter. Their registration runs later in the same startup, so
196
+ // defer the verdict and stay silent once the registry is wired.
197
+ // - The registry genuinely ends up empty: Vite's dev-server
198
+ // optimizeDeps pre-bundled this package (esbuild rewrites the globs
199
+ // to empty literals, gh#287), @phosphor-icons/core is missing, or
200
+ // the install layout is one this file doesn't probe (e.g. Yarn PnP's
201
+ // virtual filesystem). Zero icons will render — fail loudly, once,
202
+ // naming the fix (gh#340's acceptance).
203
+ // Deliberately NOT calling installIconLoaders with empty maps here:
204
+ // that would mark the registry "ready", suppressing <icon-ui>'s own
205
+ // unwired-registry diagnostic and clobbering nothing-yet for manual
206
+ // consumers.
207
+ setTimeout(() => {
208
+ if (!iconRegistryUnwired()) return; // manual registration landed — working setup, no noise
209
+ console.error(
210
+ `[icons-phosphor] Zero-config icon auto-install failed: no Phosphor ` +
211
+ `SVGs found under any known node_modules layout (probed pnpm-nested ` +
212
+ `\`../node_modules/\`, npm/yarn-hoisted \`../../../\`, and workspace-root ` +
213
+ `\`../../../node_modules/\` — all relative to icons-phosphor.js), and no ` +
214
+ `icons were registered manually. Every <icon-ui> will render blank. ` +
215
+ `Fixes, most likely first:\n` +
216
+ `(1) Vite dev-server pre-bundling emptied the globs — exclude this ` +
217
+ `package from optimizeDeps in vite.config.js:\n` +
218
+ ` optimizeDeps: { exclude: ['@adia-ai/web-components', '@adia-ai/web-modules'] }\n` +
219
+ `(2) @phosphor-icons/core is not installed, or your package manager ` +
220
+ `lays out node_modules somewhere this file doesn't probe (Yarn PnP is ` +
221
+ `not supported by the zero-config path).\n` +
222
+ `(3) Register the icons you use manually — this also silences this ` +
223
+ `error permanently:\n` +
224
+ ` import { installIconLoaders } from '@adia-ai/web-components/core/icons';\n` +
225
+ ` import caretRight from '@phosphor-icons/core/assets/regular/caret-right.svg?raw';\n` +
226
+ ` installIconLoaders({\n` +
227
+ ` regular: {\n` +
228
+ ` '/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': caretRight,\n` +
229
+ ` },\n` +
230
+ ` });\n` +
231
+ `See the JSDoc header of icons-phosphor.js for the full recipe (gh#786).`
232
+ );
233
+ }, 0);
175
234
  }
176
- installIconLoaders(weightModules);
177
235
  } else {
178
236
  // Non-Vite static serving: fetch the build-time manifest in the
179
237
  // background. No top-level await — module finishes loading immediately;
package/core/provider.js CHANGED
@@ -25,6 +25,7 @@
25
25
  import { UIElement } from './element.js';
26
26
  import { defineIfFree } from './register.js';
27
27
  import { BaseController } from './controller.js';
28
+ import { viewTransition } from '../traits/view-transition/view-transition.js';
28
29
 
29
30
  // ═══════════════════════════════════════════════════════════════
30
31
  // ADIA PROVIDER (inline — tiny base class)
@@ -163,6 +164,15 @@ export class RouteController extends BaseController {
163
164
  // ═══════════════════════════════════════════════════════════════
164
165
 
165
166
  export class UIRouter extends UIProvider {
167
+ // gh#815 slice 1 — every route/content swap this element performs (the
168
+ // fetched-content path in #loadContent AND the content-less data-route-path
169
+ // swap consumers drive directly via setRoutePath()) rides the shared
170
+ // view-transition trait. That installs `this.startTransition(callback)`,
171
+ // which no-ops to a synchronous callback under prefers-reduced-motion or
172
+ // when the API is unsupported — this element never re-implements either
173
+ // fallback itself.
174
+ static traits = [viewTransition];
175
+
166
176
  #cache = new Map();
167
177
  #ownCtrl = null;
168
178
  #lastPath = null;
@@ -195,7 +205,32 @@ export class UIRouter extends UIProvider {
195
205
  navigate(path) { this.#ctrl?.commands.navigate(path); }
196
206
  replace(path) { this.#ctrl?.commands.replace(path); }
197
207
 
208
+ // Content-less usage (gh#815 slice 1): no `route.content` to fetch — a
209
+ // consumer's own CSS reveals children via `router-ui[data-route-path="…"]`
210
+ // selectors (embedded-app's tab routers do this) and the caller just wants
211
+ // the path attribute set. Routing that swap through the same trait as
212
+ // #loadContent gives it the identical no-flash guarantee instead of every
213
+ // consumer hand-rolling its own setAttribute + reduced-motion check.
214
+ setRoutePath(path) {
215
+ if (this.getAttribute('data-route-path') === path) return;
216
+ this.startTransition(() => { this.setAttribute('data-route-path', path); });
217
+ }
218
+
198
219
  connected() {
220
+ // Sensible default (motion tokens, foundation/motion.css) — a consumer
221
+ // that wants a different feel sets data-view-transition-duration itself;
222
+ // this only fills the gap when they haven't. Set BEFORE super.connected()
223
+ // (a no-op here, but PR #819 review: viewTransition.setup() reads these
224
+ // attributes once at trait-apply time, which happens after connected()
225
+ // returns in full — keeping the defaults first in program order makes
226
+ // that dependency obvious at the call site, not just true by accident
227
+ // of UIElement's connectedCallback ordering).
228
+ if (!this.hasAttribute('data-view-transition-duration')) {
229
+ this.setAttribute('data-view-transition-duration', 'var(--a-duration)');
230
+ }
231
+ if (!this.hasAttribute('data-view-transition-easing')) {
232
+ this.setAttribute('data-view-transition-easing', 'var(--a-easing)');
233
+ }
199
234
  super.connected();
200
235
  document.addEventListener('click', this.#onClick);
201
236
  }
@@ -251,30 +286,56 @@ export class UIRouter extends UIProvider {
251
286
  if (nav !== this.#navSeq) return; // superseded while resolving
252
287
  }
253
288
 
254
- this.innerHTML = html;
255
- // Re-stamp executable <script> tags so consumer demos can wire data via
256
- // imperative JS APIs (the documented `el.items = […]` / `el.invoice = {…}`
257
- // pattern used by plan-picker, invoice-detail, list-window et al). HTML
258
- // assigned via innerHTML does NOT execute embedded <script> elements per
259
- // the HTML spec; we replicate the canonical standalone-wrapper rehydration
260
- // pattern by cloning each script + swapping the inert original. `<script
261
- // type="application/json" …>` blocks are left untouched so the receiving
262
- // custom element's connectedCallback can absorb them (the documented
263
- // in-band data pattern; see PlanPicker.#absorbInlinePlansScript et al).
264
- for (const oldScript of this.querySelectorAll('script')) {
265
- const type = oldScript.getAttribute('type') || '';
266
- // Skip data blocks those are consumed by the runtime, not executed.
267
- if (type === 'application/json' || type === 'application/ld+json') continue;
268
- const newScript = document.createElement('script');
269
- if (type) newScript.type = type;
270
- if (oldScript.src) newScript.src = oldScript.src;
271
- else newScript.textContent = oldScript.textContent;
272
- oldScript.replaceWith(newScript);
273
- }
274
- this.scrollTo(0, 0);
275
- (this.closest('section') || this.parentElement)?.scrollTo(0, 0);
289
+ // gh#815 slice 1 — the actual pane swap (old content → new) rides the
290
+ // view-transition trait so the browser cross-fades instead of a bare
291
+ // synchronous replace. startTransition() already degrades to running
292
+ // this callback synchronously under prefers-reduced-motion or when the
293
+ // API is unsupported (view-transition.js owns both fallbacks).
294
+ //
295
+ // PR #819 review (CodeRabbit) on a REAL view transition the browser
296
+ // runs this callback asynchronously (it has to capture the "old"
297
+ // snapshot first, which needs a render pass); startTransition() itself
298
+ // returns before that callback has necessarily run. Two consequences,
299
+ // both closed here:
300
+ // 1. the callback itself must re-check `nav` — a newer navigation can
301
+ // supersede this one during that async gap, same incident class
302
+ // #navSeq exists to prevent (see the class comment above), and
303
+ // without this guard a stale callback could paint old content
304
+ // OVER a newer navigation's already-painted content.
305
+ // 2. everything downstream of the paint (setup-module import,
306
+ // route-loaded emit) must wait for the paint to actually finish —
307
+ // `transition.updateCallbackDone` resolves once the callback's
308
+ // return value settles — or the setup module runs against the
309
+ // PREVIOUS route's DOM instead of the one it was written for.
310
+ const transition = this.startTransition(() => {
311
+ if (nav !== this.#navSeq) return; // superseded — never paint stale content
312
+ this.innerHTML = html;
313
+ // Re-stamp executable <script> tags so consumer demos can wire data via
314
+ // imperative JS APIs (the documented `el.items = […]` / `el.invoice = {…}`
315
+ // pattern used by plan-picker, invoice-detail, list-window et al). HTML
316
+ // assigned via innerHTML does NOT execute embedded <script> elements per
317
+ // the HTML spec; we replicate the canonical standalone-wrapper rehydration
318
+ // pattern by cloning each script + swapping the inert original. `<script
319
+ // type="application/json" …>` blocks are left untouched so the receiving
320
+ // custom element's connectedCallback can absorb them (the documented
321
+ // in-band data pattern; see PlanPicker.#absorbInlinePlansScript et al).
322
+ for (const oldScript of this.querySelectorAll('script')) {
323
+ const type = oldScript.getAttribute('type') || '';
324
+ // Skip data blocks — those are consumed by the runtime, not executed.
325
+ if (type === 'application/json' || type === 'application/ld+json') continue;
326
+ const newScript = document.createElement('script');
327
+ if (type) newScript.type = type;
328
+ if (oldScript.src) newScript.src = oldScript.src;
329
+ else newScript.textContent = oldScript.textContent;
330
+ oldScript.replaceWith(newScript);
331
+ }
332
+ this.scrollTo(0, 0);
333
+ (this.closest('section') || this.parentElement)?.scrollTo(0, 0);
276
334
 
277
- if (route.title) document.title = route.title;
335
+ if (route.title) document.title = route.title;
336
+ });
337
+ await transition.updateCallbackDone;
338
+ if (nav !== this.#navSeq) return; // superseded while the DOM update was in flight
278
339
 
279
340
  // Auto-import setup module
280
341
  if (route.content?.endsWith('.content.html')) {