@uniweb/runtime 1.3.3 → 1.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,7 +44,7 @@
44
44
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0 || ^0.28.0",
45
45
  "vite": "^7.3.1",
46
46
  "vitest": "^4.1.7",
47
- "@uniweb/build": "1.13.1"
47
+ "@uniweb/build": "1.13.6"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A page's CSS animations carry on across the browser's first render.
3
+ *
4
+ * ⭐ The browser renders over a prerendered page rather than hydrating it (`createRoot`, `index.jsx`),
5
+ * and a CSS animation's clock belongs to its element. So the commit that replaces the page restarts
6
+ * every animation in it, and a page's first commit usually lands while its entrance is still running:
7
+ * a hero already on screen would drop back to invisible and play again. `uniweb dev` never prerenders,
8
+ * so a foundation's author never sees it.
9
+ *
10
+ * Each animation of the new page takes as its start the moment the old page was first painted, which
11
+ * is when its twin started, so it goes on from where its twin was. The observer fires after the commit
12
+ * and before the browser paints, so no frame shows a restart. It disconnects at that first commit, so
13
+ * a later navigation is left as it was.
14
+ *
15
+ * ⭐ The time of first paint rather than the old animations' own start times, read before the commit:
16
+ * an animation that has ended with no fill is gone from `getAnimations()`, and its twin would play
17
+ * again.
18
+ *
19
+ * ⛔ Only a running animation on the document's timeline:
20
+ * - a paused one is waiting for something, like a reveal that starts when its section scrolls into
21
+ * view, and setting a start time would start it;
22
+ * - a scroll-driven one has a timeline of its own, whose time is no moment on this clock.
23
+ *
24
+ * It reaches CSS animations alone. One a component starts from JavaScript when it mounts never ran on
25
+ * the prerendered page: it starts at boot, over whatever that page has shown by then.
26
+ *
27
+ * Nothing to carry: an empty root (`uniweb dev`, a page served without a prerender), and a page not
28
+ * painted yet, whose first render is the first thing a visitor sees. That can include a page painted
29
+ * a frame ago, which the browser has not reported yet: its animations have hardly begun.
30
+ *
31
+ * @param {Element} root - The element React is about to render into.
32
+ */
33
+ export function carryAnimations(root) {
34
+ if (!root.firstChild || !document.getAnimations) return
35
+ new MutationObserver((_, watch) => {
36
+ watch.disconnect()
37
+ const painted = performance.getEntriesByName('first-contentful-paint')[0]?.startTime
38
+ if (painted == null) return
39
+ for (const animation of document.getAnimations()) {
40
+ if (
41
+ animation.animationName &&
42
+ animation.playState === 'running' &&
43
+ animation.timeline === document.timeline &&
44
+ root.contains(animation.effect?.target)
45
+ ) {
46
+ animation.startTime = painted
47
+ }
48
+ }
49
+ }).observe(root, { childList: true })
50
+ }
package/src/index.jsx CHANGED
@@ -12,6 +12,7 @@ import { initUniweb, decodeData } from './setup.js'
12
12
  import { wireTracker } from './wire-foundation.js'
13
13
  import { loadFoundation, loadExtensions } from './foundation-loader.js'
14
14
  import { initAppearance } from './appearance.js'
15
+ import { carryAnimations } from './carry-animations.js'
15
16
  import RuntimeProvider from './RuntimeProvider.jsx'
16
17
 
17
18
  /**
@@ -146,6 +147,8 @@ async function initRuntime(foundationSource, options = {}) {
146
147
 
147
148
  const app = <RuntimeProvider basename={routerBasename} development={development} />
148
149
 
150
+ // Armed before the render it watches for: the one that replaces a prerendered page.
151
+ carryAnimations(container)
149
152
  createRoot(container).render(app)
150
153
 
151
154
  // Log success
@@ -17,26 +17,31 @@
17
17
  * the DOM, the per-page render loop — stay in the per-environment
18
18
  * entries; these helpers cover only the environment-agnostic L2 work.
19
19
  *
20
- * Keeping this file React-free matters for the SPA bundle: `setup.js`
21
- * pulls `wire-foundation.js` directly, but it must NOT transitively
20
+ * What this file keeps out of the SPA bundle is `react-dom/server`, not
21
+ * React: `FallbackRef` below is `React.createElement` alone. `setup.js`
22
+ * pulls `wire-foundation.js` directly, so this file must NOT transitively
22
23
  * pull `ssr-renderer.js` (which imports `react-dom/server`). The L2
23
24
  * helpers therefore live here, while the L3-composing
24
25
  * `initPrerenderForLocale` lives in `ssr-renderer.js`.
25
26
  *
26
27
  * Adding a new framework-level capability:
27
- * 1. Read the foundation declaration via `foundation.default.capabilities.<name>`.
28
- * 2. Apply it to the uniweb singleton (set a slot, call a build hook,
28
+ * 1. Name it in `extractFoundationRuntime`'s lists (`@uniweb/build`'s
29
+ * `src/runtime-schema.js`): the built entry carries only what they
30
+ * name, so anything else never reaches this file.
31
+ * 2. Read the foundation declaration via `foundation.default.capabilities.<name>`.
32
+ * 3. Apply it to the uniweb singleton (set a slot, call a build hook,
29
33
  * register something on `activeWebsite`).
30
- * 3. Provide a runtime fallback if the capability is one foundations
34
+ * 4. Provide a runtime fallback if the capability is one foundations
31
35
  * may legitimately not declare (see `FallbackRef`).
32
36
  *
33
37
  * Foundation export shape contract: the runtime always loads the
34
38
  * **built** foundation artifact (`dist/entry.js`) via
35
39
  * `loadFoundation()` in `foundation-loader.js`, which does `import(url)`
36
40
  * and returns a module namespace. The build pipeline
37
- * (`@uniweb/build`'s `src/generate-entry.js`) wraps the foundation's
38
- * source default export under `default.capabilities.*`, so the runtime
39
- * sees a single canonical shape with no need for fallback chains. This
41
+ * (`@uniweb/build`'s `src/generate-entry.js`) writes what the runtime
42
+ * reads of the foundation's source default export into
43
+ * `default.capabilities.*`, so the runtime sees a single canonical shape
44
+ * with no need for fallback chains. This
40
45
  * differs from `@uniweb/press` / `@uniweb/unipress`,
41
46
  * which DO need to handle a second shape because they're sometimes
42
47
  * called from inside a foundation bundle (where the foundation imports