@uniweb/runtime 0.6.39 → 0.7.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 +3 -3
- package/src/components/Layout.jsx +15 -3
- package/src/components/PageRenderer.jsx +14 -0
- package/src/hooks/useLinkInterceptor.js +12 -1
- package/src/setup.js +94 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@uniweb/theming": "0.1.3",
|
|
39
|
-
"@uniweb/core": "0.
|
|
39
|
+
"@uniweb/core": "0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@vitejs/plugin-react": "^4.5.2",
|
|
43
43
|
"vite": "^7.3.1",
|
|
44
|
-
"@uniweb/build": "0.8.
|
|
44
|
+
"@uniweb/build": "0.8.42"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -90,11 +90,23 @@ export default function Layout({ page, website }) {
|
|
|
90
90
|
const allBlockGroups = [bodyBlocks, ...Object.values(areas)]
|
|
91
91
|
initializeAllBlocks(...allBlockGroups)
|
|
92
92
|
|
|
93
|
-
// Pre-render each area as React elements
|
|
94
|
-
|
|
93
|
+
// Pre-render each area as React elements.
|
|
94
|
+
// When the foundation enables view transitions, wrap areas in thin divs
|
|
95
|
+
// with view-transition-name so the browser can animate them independently.
|
|
96
|
+
const transitions = website.viewTransitions ? layoutMeta?.transitions : null
|
|
97
|
+
|
|
98
|
+
const bodyElement = bodyBlocks ? (
|
|
99
|
+
transitions?.body
|
|
100
|
+
? <div style={{ viewTransitionName: transitions.body }}><Blocks blocks={bodyBlocks} /></div>
|
|
101
|
+
: <Blocks blocks={bodyBlocks} />
|
|
102
|
+
) : null
|
|
103
|
+
|
|
95
104
|
const areaElements = {}
|
|
96
105
|
for (const [name, blocks] of Object.entries(areas)) {
|
|
97
|
-
|
|
106
|
+
const transitionName = transitions?.[name]
|
|
107
|
+
areaElements[name] = transitionName
|
|
108
|
+
? <div style={{ viewTransitionName: transitionName }}><Blocks blocks={blocks} /></div>
|
|
109
|
+
: <Blocks blocks={blocks} />
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
// Use foundation's custom Layout if provided
|
|
@@ -127,6 +127,16 @@ export default function PageRenderer() {
|
|
|
127
127
|
let page = website?.getPage(location.pathname)
|
|
128
128
|
if (page && website) website.setActivePage(location.pathname)
|
|
129
129
|
|
|
130
|
+
// ─── Content loading (split page content) ───
|
|
131
|
+
// On first load, the current page's sections are pre-embedded → contentReady = true.
|
|
132
|
+
// On SPA navigation to an unvisited page → contentReady = false → fetch → re-render.
|
|
133
|
+
const contentReady = !page || !page.hasContent() || page.isContentLoaded()
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (contentReady) return
|
|
137
|
+
page.loadContent().then(() => forceUpdate())
|
|
138
|
+
}, [page, contentReady])
|
|
139
|
+
|
|
130
140
|
// ─── Compute navigation targets (before hooks, no early returns) ───
|
|
131
141
|
|
|
132
142
|
// Explicit redirect: in page.yml
|
|
@@ -213,6 +223,10 @@ export default function PageRenderer() {
|
|
|
213
223
|
)
|
|
214
224
|
}
|
|
215
225
|
|
|
226
|
+
// Wait for content before rendering blocks (split page content).
|
|
227
|
+
// The prerendered HTML is already visible while we load.
|
|
228
|
+
if (!contentReady) return null
|
|
229
|
+
|
|
216
230
|
const appearance = website?.themeData?.appearance
|
|
217
231
|
|
|
218
232
|
// Use Layout component for proper orchestration
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useCallback } from 'react'
|
|
16
16
|
import { useNavigate, useLocation } from 'react-router-dom'
|
|
17
|
+
import { prefetchContent, prefersReducedMotion } from '../setup.js'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Check if a URL is internal (same origin, no external protocol)
|
|
@@ -193,7 +194,17 @@ export function useLinkInterceptor(options = {}) {
|
|
|
193
194
|
// Use React Router navigation (with router-relative path)
|
|
194
195
|
// React Router will handle the path, and our useEffect above
|
|
195
196
|
// will handle scrolling to hash after navigation completes
|
|
196
|
-
|
|
197
|
+
const useTransition = website?.viewTransitions && document.startViewTransition
|
|
198
|
+
&& !prefersReducedMotion
|
|
199
|
+
|
|
200
|
+
if (useTransition) {
|
|
201
|
+
document.startViewTransition(async () => {
|
|
202
|
+
await prefetchContent(routeHref)
|
|
203
|
+
navigate(routeHref)
|
|
204
|
+
})
|
|
205
|
+
} else {
|
|
206
|
+
navigate(routeHref)
|
|
207
|
+
}
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
// Add click listener to document
|
package/src/setup.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* with routing components, icon resolver, data fetcher, etc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import React from 'react'
|
|
8
9
|
import { createUniweb, Website } from '@uniweb/core'
|
|
9
10
|
import {
|
|
10
11
|
Link as RouterLink,
|
|
11
|
-
useNavigate,
|
|
12
|
+
useNavigate as useRouterNavigate,
|
|
12
13
|
useParams,
|
|
13
14
|
useLocation
|
|
14
15
|
} from 'react-router-dom'
|
|
@@ -16,6 +17,97 @@ import {
|
|
|
16
17
|
import { ChildBlocks } from './components/PageRenderer.jsx'
|
|
17
18
|
import { executeFetchClient } from './data-fetcher-client.js'
|
|
18
19
|
|
|
20
|
+
// ─── View Transition Wrappers ───────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// When the foundation enables viewTransitions, navigation is wrapped in
|
|
23
|
+
// document.startViewTransition() to animate page changes. Split content
|
|
24
|
+
// is prefetched inside the transition callback so the user never sees
|
|
25
|
+
// a blank or loading state.
|
|
26
|
+
//
|
|
27
|
+
// These wrappers are registered as routing components, so Kit's <Link>
|
|
28
|
+
// and useRouting().useNavigate() automatically get view transition support.
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Prefetch split page content with a timeout.
|
|
32
|
+
* Resolves when content is loaded or the timeout expires — whichever
|
|
33
|
+
* comes first. This keeps the old-page screenshot from freezing too
|
|
34
|
+
* long on slow connections. If the timeout wins, navigation proceeds
|
|
35
|
+
* and the PageRenderer loading gate handles the rest.
|
|
36
|
+
*/
|
|
37
|
+
const CONTENT_PREFETCH_TIMEOUT = 1000
|
|
38
|
+
export const prefersReducedMotion = typeof window !== 'undefined'
|
|
39
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
40
|
+
|
|
41
|
+
export function prefetchContent(route) {
|
|
42
|
+
const website = globalThis.uniweb?.activeWebsite
|
|
43
|
+
if (!route || !website) return
|
|
44
|
+
|
|
45
|
+
const targetPage = website.getPage(route)
|
|
46
|
+
if (!targetPage?.hasContent?.() || targetPage.isContentLoaded?.()) return
|
|
47
|
+
|
|
48
|
+
return Promise.race([
|
|
49
|
+
targetPage.loadContent(),
|
|
50
|
+
new Promise(resolve => setTimeout(resolve, CONTENT_PREFETCH_TIMEOUT))
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prefetch split content and navigate inside a view transition.
|
|
56
|
+
* Falls back to plain navigation when transitions are not available.
|
|
57
|
+
*/
|
|
58
|
+
function navigateWithTransition(navigate, to, options) {
|
|
59
|
+
const vt = globalThis.uniweb?.foundationConfig?.viewTransitions
|
|
60
|
+
if (vt && document.startViewTransition && !prefersReducedMotion) {
|
|
61
|
+
document.startViewTransition(async () => {
|
|
62
|
+
const route = typeof to === 'string' ? to : to?.pathname
|
|
63
|
+
await prefetchContent(route)
|
|
64
|
+
navigate(to, options)
|
|
65
|
+
})
|
|
66
|
+
} else {
|
|
67
|
+
navigate(to, options)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* View-transition-aware Link component.
|
|
73
|
+
* Intercepts clicks to wrap navigation in startViewTransition() when enabled.
|
|
74
|
+
* Falls through to RouterLink's normal behavior otherwise.
|
|
75
|
+
* Preserves all React Router Link props (replace, state, preventScrollReset).
|
|
76
|
+
*/
|
|
77
|
+
const ViewTransitionLink = React.forwardRef(function ViewTransitionLink(
|
|
78
|
+
{ onClick, replace, state, preventScrollReset, ...props },
|
|
79
|
+
ref
|
|
80
|
+
) {
|
|
81
|
+
const navigate = useRouterNavigate()
|
|
82
|
+
|
|
83
|
+
const handleClick = (e) => {
|
|
84
|
+
if (onClick) onClick(e)
|
|
85
|
+
if (e.defaultPrevented) return
|
|
86
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
|
|
87
|
+
if (e.button !== 0) return
|
|
88
|
+
|
|
89
|
+
const vt = globalThis.uniweb?.foundationConfig?.viewTransitions
|
|
90
|
+
if (!vt || !document.startViewTransition || prefersReducedMotion) return
|
|
91
|
+
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
navigateWithTransition(navigate, props.to, { replace, state, preventScrollReset })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return React.createElement(RouterLink, {
|
|
97
|
+
ref, ...props, onClick: handleClick,
|
|
98
|
+
replace, state, preventScrollReset
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* View-transition-aware useNavigate hook.
|
|
104
|
+
* The returned function wraps navigation in startViewTransition() when enabled.
|
|
105
|
+
*/
|
|
106
|
+
function useNavigate() {
|
|
107
|
+
const navigate = useRouterNavigate()
|
|
108
|
+
return (to, options) => navigateWithTransition(navigate, to, options)
|
|
109
|
+
}
|
|
110
|
+
|
|
19
111
|
/**
|
|
20
112
|
* Map friendly family names to react-icons codes
|
|
21
113
|
* The existing CDN uses react-icons structure: /{familyCode}/{familyCode}-{name}.svg
|
|
@@ -139,7 +231,7 @@ export function setupUniweb(configData) {
|
|
|
139
231
|
// This enables the bridge pattern: components access routing via
|
|
140
232
|
// website.getRoutingComponents() instead of direct imports
|
|
141
233
|
uniwebInstance.routingComponents = {
|
|
142
|
-
Link:
|
|
234
|
+
Link: ViewTransitionLink,
|
|
143
235
|
useNavigate,
|
|
144
236
|
useParams,
|
|
145
237
|
useLocation
|