@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "0.6.39",
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.5.22"
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.41"
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
- const bodyElement = bodyBlocks ? <Blocks blocks={bodyBlocks} /> : null
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
- areaElements[name] = <Blocks blocks={blocks} />
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
- navigate(routeHref)
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: RouterLink,
234
+ Link: ViewTransitionLink,
143
235
  useNavigate,
144
236
  useParams,
145
237
  useLocation