@uniweb/runtime 1.2.0 → 1.2.1

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.
@@ -2,11 +2,13 @@
2
2
  * SSR Renderer
3
3
  *
4
4
  * Hook-free rendering pipeline for SSG (build) and server-side SSR.
5
- * Mirrors BlockRenderer.jsx + Background.jsx using React.createElement
6
- * directly — no hooks, no JSX, no browser APIs.
5
+ * Mirrors BlockRenderer.jsx using React.createElement directly — no hooks,
6
+ * no JSX, no browser APIs. A section's background and the built-in layout are
7
+ * not mirrored: both renderers draw the one copy (`background.js`,
8
+ * `default-layout.js`).
7
9
  *
8
10
  * This is the single source of truth for how blocks render during prerender.
9
- * When modifying BlockRenderer.jsx or Background.jsx, update this file to match.
11
+ * When modifying BlockRenderer.jsx, update `renderBlock` here to match.
10
12
  *
11
13
  * Exports three layers:
12
14
  * 1. Rendering functions (renderBlock, renderBlocks, renderLayout, renderBackground)
@@ -18,7 +20,7 @@ import React from 'react'
18
20
  import { renderToString } from 'react-dom/server'
19
21
  import { createUniweb, resolveDefaultLocale } from '@uniweb/core'
20
22
  import { sectionDomId } from '@uniweb/core/section-id'
21
- import { siteUrl, withOpacity } from './background-shared.js'
23
+ import { Background } from './background.js'
22
24
  import { routePatternToRegex, isDynamicRoute } from '@uniweb/core/route-match'
23
25
  import { DEFAULT_ICON_BASE, iconUrl } from '@uniweb/core/icon-corpus'
24
26
  import { buildSectionOverrides, FONT_LINKS_MARKER } from '@uniweb/theming'
@@ -95,123 +97,17 @@ export function getWrapperProps(block) {
95
97
 
96
98
 
97
99
  /**
98
- * Render a background element for SSR.
99
- * Mirrors Background.jsx (color, gradient, image — not video).
100
- * Video backgrounds require JS for autoplay and are skipped during SSR.
100
+ * Render a section's background for SSR: the one implementation both renderers draw
101
+ * (`background.js`). A video background is drawn as its poster — what the browser draws too,
102
+ * until it has mounted and starts the video.
103
+ * ⛔ Until 2026-10-25 this was a copy of `Background.jsx`, and drew no video at all.
104
+ *
105
+ * @param {Object} background - `block.background`, normalized by core
106
+ * @returns {React.ReactElement|null}
101
107
  */
102
108
  export function renderBackground(background) {
103
109
  if (!background?.mode) return null
104
-
105
- const containerStyle = {
106
- position: 'absolute',
107
- inset: '0',
108
- overflow: 'hidden',
109
- zIndex: 0,
110
- }
111
-
112
- const children = []
113
-
114
- // Color background
115
- if (background.mode === 'color' && background.color) {
116
- children.push(
117
- React.createElement('div', {
118
- key: 'bg-color',
119
- className: 'background-color',
120
- style: { position: 'absolute', inset: '0', backgroundColor: background.color },
121
- 'aria-hidden': 'true',
122
- })
123
- )
124
- }
125
-
126
- // Gradient background (supports string or object with opacity)
127
- if (background.mode === 'gradient' && background.gradient) {
128
- const g = background.gradient
129
-
130
- let bgValue
131
- if (typeof g === 'string') {
132
- bgValue = g
133
- } else {
134
- const {
135
- start = 'transparent',
136
- end = 'transparent',
137
- angle = 0,
138
- startPosition = 0,
139
- endPosition = 100,
140
- startOpacity = 1,
141
- endOpacity = 1,
142
- } = g
143
- const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start
144
- const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end
145
- bgValue = `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`
146
- }
147
-
148
- children.push(
149
- React.createElement('div', {
150
- key: 'bg-gradient',
151
- className: 'background-gradient',
152
- style: { position: 'absolute', inset: '0', background: bgValue },
153
- 'aria-hidden': 'true',
154
- })
155
- )
156
- }
157
-
158
- // Image background
159
- if (background.mode === 'image' && background.image?.src) {
160
- const img = background.image
161
- children.push(
162
- React.createElement('div', {
163
- key: 'bg-image',
164
- className: 'background-image',
165
- style: {
166
- position: 'absolute',
167
- inset: '0',
168
- backgroundImage: `url(${siteUrl(img.src)})`,
169
- backgroundPosition: img.position || 'center',
170
- backgroundSize: img.size || 'cover',
171
- backgroundRepeat: 'no-repeat',
172
- },
173
- 'aria-hidden': 'true',
174
- })
175
- )
176
- }
177
-
178
- // Overlay (gradient or solid)
179
- if (background.overlay?.enabled) {
180
- const ov = background.overlay
181
- let overlayStyle
182
-
183
- if (ov.gradient) {
184
- const g = ov.gradient
185
- overlayStyle = {
186
- position: 'absolute', inset: '0', pointerEvents: 'none',
187
- background: `linear-gradient(${g.angle || 180}deg, ${g.start || 'rgba(0,0,0,0.7)'} ${g.startPosition || 0}%, ${g.end || 'rgba(0,0,0,0)'} ${g.endPosition || 100}%)`,
188
- opacity: ov.opacity ?? 0.5,
189
- }
190
- } else {
191
- const baseColor = ov.type === 'light' ? '255, 255, 255' : '0, 0, 0'
192
- overlayStyle = {
193
- position: 'absolute', inset: '0', pointerEvents: 'none',
194
- backgroundColor: `rgba(${baseColor}, ${ov.opacity ?? 0.5})`,
195
- }
196
- }
197
-
198
- children.push(
199
- React.createElement('div', {
200
- key: 'bg-overlay',
201
- className: ov.gradient ? 'background-overlay background-overlay--gradient' : 'background-overlay background-overlay--solid',
202
- style: overlayStyle,
203
- 'aria-hidden': 'true',
204
- })
205
- )
206
- }
207
-
208
- if (children.length === 0) return null
209
-
210
- return React.createElement('div', {
211
- className: `background background--${background.mode}`,
212
- style: containerStyle,
213
- 'aria-hidden': 'true',
214
- }, ...children)
110
+ return React.createElement(Background, background)
215
111
  }
216
112
 
217
113
  /**
@@ -244,11 +140,20 @@ export function renderBlock(block, { as = 'section', stickySelf = false } = {})
244
140
  const meta = getComponentMeta(block.type)
245
141
  const entityStore = block.website?.entityStore
246
142
  let entityData = null
143
+ let pending = false
247
144
  if (entityStore) {
248
145
  const resolved = entityStore.resolve(block, meta)
249
146
  if (resolved.status === 'ready') entityData = resolved.data
147
+ pending = resolved.status === 'pending'
250
148
  }
251
149
 
150
+ // ⭐ A block whose data is still out is LOADING here, as in the browser, which sets the same flag
151
+ // before `prepareProps` (`BlockRenderer.jsx`) and fetches the data once it boots. So the page
152
+ // paints the component's loading state, the one the browser renders in its place.
153
+ // ⛔ Until 2026-10-25 this was never set, and such a section prerendered its EMPTY state —
154
+ // "nothing found", or an error — then switched to loading when the browser took over.
155
+ block.dataLoading = pending
156
+
252
157
  // Build content and params with runtime guarantees.
253
158
  // prepareProps handles the full pipeline: entity data merge,
254
159
  // foundation content handler invocation, guaranteed content
@@ -475,10 +380,9 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
475
380
  }
476
381
 
477
382
  // The same background a section gets, for kit's `SectionBackground` — a component that
478
- // paints its own (`background: 'self'`). Mirrors the SPA's `Background` (setup.js).
479
- uniweb.backgroundRenderer = function InlineBackground(background) {
480
- return renderBackground(background)
481
- }
383
+ // paints its own (`background: 'self'`). The one implementation, which the browser draws
384
+ // too (setup.js); its `className` is the layer's. ⛔ Until 2026-10-25 this dropped it.
385
+ uniweb.backgroundRenderer = Background
482
386
 
483
387
  // L2 (singleton wiring): defaultInsets, xref.build(), and any future
484
388
  // framework-level capability bridge — shared with setup.js so both