@uniweb/runtime 1.5.1 → 1.5.2

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.5.1",
3
+ "version": "1.5.2",
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.6.7",
40
- "@uniweb/theming": "~1.3.1"
39
+ "@uniweb/theming": "~1.3.1",
40
+ "@uniweb/core": "~1.6.7"
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.18.0"
47
+ "@uniweb/build": "1.18.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -35,7 +35,7 @@ import { useEffect } from 'react'
35
35
  import Blocks from './Blocks.jsx'
36
36
  import { resolveLayoutTransitions, resolveLayoutLayers, areaWrapperProps } from '../area-wrappers.js'
37
37
  import { layoutStickiness, zoneProps } from '../sticky.js'
38
- import { headerOverlay, overlayBoxProps } from '../under-header.js'
38
+ import { headerOverlay, overlayBoxProps, headerAreaProps } from '../under-header.js'
39
39
  import { warnUnmovableSticky } from '../sticky-check.js'
40
40
  import { watchStickyTop, bodySectionOf } from '../sticky-top.js'
41
41
  // The built-in layout — one copy, which the prerender renders too (`../default-layout.js`).
@@ -102,8 +102,12 @@ export default function Layout({ page, website }) {
102
102
  const overlay = headerOverlay(page)
103
103
 
104
104
  const wrap = (region, children) => {
105
- let props = areaWrapperProps(region, transitions, layers, RemoteLayout ? sticky[region] : null)
106
- if (overlay && RemoteLayout && region === 'header') props = overlayBoxProps(props)
105
+ const props = areaWrapperProps(region, transitions, layers, RemoteLayout ? sticky[region] : null)
106
+ if (RemoteLayout && region === 'header') {
107
+ // The same boxes on every page, the name on one that keeps the header's height.
108
+ const { outer, inner } = headerAreaProps(props, overlay)
109
+ return <div {...outer}>{inner ? <div {...inner}>{children}</div> : children}</div>
110
+ }
107
111
  return props ? <div {...props}>{children}</div> : children
108
112
  }
109
113
 
@@ -34,7 +34,7 @@ import {
34
34
  } from './wire-foundation.js'
35
35
  import { resolveLayoutTransitions, resolveLayoutLayers, areaWrapperProps } from './area-wrappers.js'
36
36
  import { layoutStickiness, sectionStickyProps, sectionScrollMargin, zoneProps } from './sticky.js'
37
- import { headerOverlay, overlayBoxProps, openingUnderHeader, underHeaderProps } from './under-header.js'
37
+ import { headerOverlay, overlayBoxProps, openingUnderHeader, underHeaderProps, headerAreaProps } from './under-header.js'
38
38
  import { DefaultLayout } from './default-layout.js'
39
39
  import { renderAppearanceBootScript } from './appearance.js'
40
40
  import { documentTitle } from './document-title.js'
@@ -274,8 +274,12 @@ export function renderLayout(page, website) {
274
274
  // Mirror Layout.jsx: the header lies over a page's opening section under it (`under-header.js`).
275
275
  const overlay = headerOverlay(page)
276
276
  const wrapArea = (name, element) => {
277
- let props = areaWrapperProps(name, transitions, layers, RemoteLayout ? sticky[name] : null)
278
- if (overlay && RemoteLayout && name === 'header') props = overlayBoxProps(props)
277
+ const props = areaWrapperProps(name, transitions, layers, RemoteLayout ? sticky[name] : null)
278
+ if (RemoteLayout && name === 'header') {
279
+ // Mirror Layout.jsx: the same boxes on every page, the name on one that keeps the header's height.
280
+ const { outer, inner } = headerAreaProps(props, overlay)
281
+ return React.createElement('div', outer, inner ? React.createElement('div', inner, element) : element)
282
+ }
279
283
  return props ? React.createElement('div', props, element) : element
280
284
  }
281
285
 
package/src/sticky-top.js CHANGED
@@ -65,14 +65,19 @@ function drawnHeight(box) {
65
65
  }
66
66
 
67
67
  /**
68
- * How tall the header lying over the page's opening section is, in px — null on a page with none.
68
+ * How tall the header lying over the page's opening section is, in px — 0 when the layout drew no
69
+ * header on such a page, null on a page with no section under the header.
69
70
  *
70
71
  * @param {Document} doc
71
72
  * @returns {number|null}
72
73
  */
73
74
  export function measureHeaderOverlay(doc) {
74
75
  const box = doc.querySelector('[data-uw-header="over"]')
75
- return box ? Math.round(drawnHeight(box) * 100) / 100 : null
76
+ if (box) return Math.round(drawnHeight(box) * 100) / 100
77
+ // A page whose opening section lies under the header, in a layout that drew no header on it:
78
+ // there is nothing to clear, so the section is padded by nothing once the browser has looked.
79
+ // ⛔ Until 2026-11-06 it kept the declared height, padding the section for a header not there.
80
+ return doc.querySelector('[data-uw-under-header]') ? 0 : null
76
81
  }
77
82
 
78
83
  /**
@@ -57,6 +57,36 @@ export function overlayBoxProps(props) {
57
57
  return { ...(props || {}), style: { ...(props?.style || {}), height: 0, minHeight: 0 }, 'data-uw-header': 'over' }
58
58
  }
59
59
 
60
+ /**
61
+ * The boxes a foundation layout's header area is drawn in, the same in both renderers.
62
+ *
63
+ * ⭐ THE SAME ELEMENTS ON EVERY PAGE. Between a page whose opening section lies under the header and
64
+ * one with none, only styles change, so React keeps the header mounted, with its state: a search
65
+ * box's words, a menu, an animation that ran once. So the area always has its wrapper — its layer
66
+ * and stickiness, collapsed on a page under the header, and `display: contents`, no box at all,
67
+ * where it has nothing to carry (a layout that ranks its own stacking and a header that neither
68
+ * sticks nor is named). ⛔ For part of 2026-11-06 a wrapper appeared only on a page under the
69
+ * header there, and React rebuilt the header on each move between the two kinds of page.
70
+ *
71
+ * ⭐ THE NAME A LAYOUT ASKS FOR GOES ON AN ELEMENT INSIDE THE WRAPPER, which keeps the header's
72
+ * height on every page. A named box is captured at its own size, and one with no height is captured
73
+ * as nothing, so on the collapsed wrapper the header was gone for the length of a page switch; the
74
+ * element inside is the header's extent everywhere, and two pages' headers pair as the layout means.
75
+ * ⛔ For part of 2026-11-06 a collapsed wrapper dropped the name instead, which quietly ignored what
76
+ * the layout asked for on exactly those pages.
77
+ *
78
+ * @param {Object|null} wrapper - the area wrapper's props (`areaWrapperProps`), its name included
79
+ * @param {{ height: string }|null} overlay - `headerOverlay(page)`
80
+ * @returns {{ outer: Object, inner: Object|null }} the wrapper's props, and the named element's
81
+ */
82
+ export function headerAreaProps(wrapper, overlay) {
83
+ const { viewTransitionName, ...style } = wrapper?.style || {}
84
+ const inner = viewTransitionName ? { style: { viewTransitionName } } : null
85
+ const base = { ...(wrapper || {}), style }
86
+ if (overlay) return { outer: overlayBoxProps(base), inner }
87
+ return { outer: Object.keys(style).length ? base : { ...base, style: { display: 'contents' } }, inner }
88
+ }
89
+
60
90
  /**
61
91
  * What the opening section's wrapper adds when the header lies over it: the header's height, the
62
92
  * declared one until the browser has measured it.