@uniweb/runtime 1.3.3 → 1.3.4

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.4",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -36,15 +36,15 @@
36
36
  "node": ">=22.12"
37
37
  },
38
38
  "dependencies": {
39
- "@uniweb/core": "~1.5.1",
40
- "@uniweb/theming": "~1.2.1"
39
+ "@uniweb/theming": "~1.2.1",
40
+ "@uniweb/core": "~1.5.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@vitejs/plugin-react": "^4.5.2",
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.5"
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