@uniweb/runtime 1.2.13 → 1.3.0

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.2.13",
3
+ "version": "1.3.0",
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/theming": "~1.2.1",
40
- "@uniweb/core": "~1.4.12"
39
+ "@uniweb/core": "~1.5.0",
40
+ "@uniweb/theming": "~1.2.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.10.14"
47
+ "@uniweb/build": "1.12.2"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -2,11 +2,19 @@
2
2
  * What the runtime puts on a layout-area wrapper.
3
3
  *
4
4
  * When a foundation enables view transitions (the default), the runtime gives
5
- * each layout region a `view-transition-name` so the browser animates them
6
- * independently — persistent chrome (header, sidebar, footer) morphs in place
7
- * while the body crossfades. Without per-region names the browser falls back to
8
- * a single full-page crossfade, which makes the whole layout (chrome included)
9
- * flicker on every navigation.
5
+ * each layout area a `view-transition-name` so the browser animates it on its
6
+ * own — persistent chrome (header, sidebar, footer) morphs in place while
7
+ * everything else, the body included, crossfades as the page. With no names the
8
+ * whole layout (chrome included) would flicker on every navigation.
9
+ *
10
+ * ⛔ **The body takes no name by default, which is what keeps a phone usable.** A
11
+ * named element is captured as one image. WebKit captures it whole, off-screen part
12
+ * included, at device resolution, painting every filter in it on the main thread; the
13
+ * page itself is captured at the size of the viewport. Measured 2026-10-30 on a live
14
+ * site, in WebKit on a 3× phone viewport with a desktop CPU: a named body was a
15
+ * 1179 × 22710 capture, 107 MB against 9 MB, and a tap stalled the main thread for up to
16
+ * 842 ms, against 271 ms with no name. Chromium did not stall at all. On an iPhone, that
17
+ * site froze for about 30 s on a tap. The body still crossfades, with the page.
10
18
  *
11
19
  * Naming is only half of it. A `view-transition-name` **makes its element a
12
20
  * stacking context**, so the moment the runtime adds these wrappers it has
@@ -41,13 +49,14 @@ const toIdent = (name) => NS + String(name).replace(/[^a-zA-Z0-9_-]/g, '-')
41
49
  /**
42
50
  * Build the effective view-transition-name map for a layout.
43
51
  *
44
- * Default: every rendered area plus the implicit `body` gets a stable,
45
- * namespaced name (`uw-<area>`, `uw-body`). Same-named areas across layouts
46
- * therefore share a name and morph between layouts automatically.
52
+ * Default: every rendered area gets a stable, namespaced name (`uw-<area>`), so
53
+ * same-named areas across layouts share a name and morph between layouts
54
+ * automatically. The body gets none and crossfades as the page (the header above).
47
55
  *
48
56
  * The layout's `meta.js` `transitions` value overrides this:
49
57
  * - an object overrides per region (`{ left: 'sidebar' }` to group across
50
- * layouts, or `{ left: null }` to opt one region out);
58
+ * layouts, `{ left: null }` to opt one region out, or `{ body: 'main' }` for a
59
+ * body that morphs between layouts, which WebKit captures whole);
51
60
  * - `false` opts the whole layout out (back to the full-page crossfade).
52
61
  *
53
62
  * @param {string[]} areaNames - Names of the areas rendered for this page (excludes `body`).
@@ -58,7 +67,7 @@ const toIdent = (name) => NS + String(name).replace(/[^a-zA-Z0-9_-]/g, '-')
58
67
  export function resolveLayoutTransitions(areaNames, explicit) {
59
68
  if (explicit === false) return null
60
69
 
61
- const transitions = { body: toIdent('body') }
70
+ const transitions = {}
62
71
  for (const name of areaNames) transitions[name] = toIdent(name)
63
72
 
64
73
  return explicit ? { ...transitions, ...explicit } : transitions
@@ -169,6 +178,7 @@ export function areaWrapperStyle(region, transitions, layers, sticky = null) {
169
178
 
170
179
  const name = transitions?.[region]
171
180
  if (name) style.viewTransitionName = name
181
+ else if (transitions && region === 'body') style.isolation = 'isolate'
172
182
 
173
183
  const layer = layers?.[region]
174
184
  if (sticky) {
@@ -139,8 +139,8 @@ export default function BlockRenderer({ block, as = 'section', stickySelf = fals
139
139
 
140
140
  // Build content and params with runtime guarantees.
141
141
  // prepareProps handles the full pipeline: entity data merge onto
142
- // block.parsedContent.data, foundation content handler invocation,
143
- // guaranteed content structure, schema application, and param defaults.
142
+ // block.parsedContent.data, foundation data and content handlers,
143
+ // param defaults, guaranteed content structure, and the props handler.
144
144
  // See prepare-props.js for the pipeline details.
145
145
  const prepared = prepareProps(block, meta, entityData)
146
146
  const params = prepared.params
@@ -86,8 +86,8 @@ export default function Layout({ page, website }) {
86
86
  // Pre-render each area as React elements.
87
87
  // When the foundation enables view transitions, wrap areas in thin divs with
88
88
  // view-transition-name so the browser can animate them independently. Names
89
- // default to one per area + body (see resolveLayoutTransitions); the layout's
90
- // `transitions` meta overrides or opts out.
89
+ // default to one per area and none for the body (see resolveLayoutTransitions);
90
+ // the layout's `transitions` meta overrides or opts out.
91
91
  const areaNames = Object.keys(areas)
92
92
  const transitions = website.viewTransitions
93
93
  ? resolveLayoutTransitions(areaNames, layoutMeta?.transitions)
@@ -157,8 +157,8 @@ export function renderBlock(block, { as = 'section', stickySelf = false } = {})
157
157
 
158
158
  // Build content and params with runtime guarantees.
159
159
  // prepareProps handles the full pipeline: entity data merge,
160
- // foundation content handler invocation, guaranteed content
161
- // structure, schema application, and param defaults.
160
+ // foundation data and content handlers, param defaults, guaranteed
161
+ // content structure, and the props handler.
162
162
  // See prepare-props.js for the pipeline details.
163
163
  const prepared = prepareProps(block, meta, entityData)
164
164
  const params = prepared.params
@@ -247,7 +247,7 @@ export function renderLayout(page, website) {
247
247
  for (const block of blocks || []) block.initComponent()
248
248
  }
249
249
 
250
- // Mirror Layout.jsx: wrap body + each area in a thin div carrying its
250
+ // Mirror Layout.jsx: wrap the body and each area in a thin div, an area's carrying its
251
251
  // view-transition-name, so the prerendered HTML matches the page the SPA renders
252
252
  // in its place and the browser can animate regions independently on client navigation.
253
253
  const areaNames = Object.keys(areas)