@brust/runtime-dom 2.0.0-alpha.28 → 2.0.0-alpha.29

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": "@brust/runtime-dom",
3
- "version": "2.0.0-alpha.28",
3
+ "version": "2.0.0-alpha.29",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/nav/nav.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPA navigation (design): a qualifying link click, `navigate()` or
2
2
  // back/forward fetches the real HTML document and swaps it in place, so the page shows exactly what a full load of
3
3
  // that URL shows (§2). Anything that cannot be swapped is a full load. Events: `brust:nav:click|load|success|error`.
4
- import { applyHead, classicScripts, planHead, runScripts, swapBody } from './swap'
4
+ import { applyHead, classicScripts, incomingImages, planHead, runScripts, swapBody } from './swap'
5
5
 
6
6
  type Trigger = 'link' | 'navigate' | 'popstate'
7
7
  export interface NavBridge { navigate(url: string, opts?: { replace?: boolean }): Promise<void>; clearCache(): void }
@@ -71,7 +71,8 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
71
71
  }
72
72
 
73
73
  // ---- the progress bar (§10.2): only for a navigation still loading 150 ms after `load`; outside <body>, so no swap
74
- // removes it, and its <style> is runtime-added, so the head reconcile keeps it ----
74
+ // removes it, and its <style> is runtime-added, so the head reconcile keeps it. The <style> goes first in the head
75
+ // and its rules are one attribute selector each, so an app rule on div[data-brust-bar] wins ----
75
76
  let bar: Element | undefined
76
77
  let css: HTMLStyleElement | undefined
77
78
  let wait: ReturnType<typeof setTimeout> | undefined
@@ -81,12 +82,12 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
81
82
  if (!bar && html.getAttribute('data-brust-progress') !== 'off') wait = setTimeout(() => {
82
83
  if (!css) {
83
84
  ;(css = document.createElement('style')).textContent =
84
- `div[${BAR}]{position:fixed;z-index:2147483647;top:0;left:0;width:100%;height:3px;pointer-events:none;background:var(--brust-progress-color,#2f7cf6);transform-origin:0;transform:scaleX(0);animation:brust-bar 8s cubic-bezier(.1,.7,.3,1) forwards}` +
85
- `div[${BAR}=done]{animation:none;transform:none;opacity:0;transition:transform .2s,opacity .3s .2s}@keyframes brust-bar{to{transform:scaleX(.9)}}` +
86
- `@media (prefers-reduced-motion:reduce){div[${BAR}]{animation:none;transform:scaleX(.9)}}`
85
+ `div[${BAR}]{position:fixed;z-index:2147483647;top:0;left:0;width:100%;height:var(--brust-progress-height,2px);pointer-events:none;background:var(--brust-progress-color,#2f7cf6);transform-origin:0;transform:scaleX(0);animation:brust-bar 8s cubic-bezier(.1,.8,.2,1) forwards}` +
86
+ `div[${BAR}=done]{animation:none;transform:none;opacity:0;transition:transform .2s,opacity .3s .2s}@keyframes brust-bar{to{transform:scaleX(.8)}}` +
87
+ `@media (prefers-reduced-motion:reduce){div[${BAR}]{animation:none;transform:scaleX(.8)}div[${BAR}=done]{transition:none}}`
87
88
  css.setAttribute(BAR, '')
88
89
  }
89
- if (!css.isConnected) document.head.append(css)
90
+ if (!css.isConnected) document.head.prepend(css)
90
91
  html.append((bar = document.createElement('div')))
91
92
  bar.setAttribute(BAR, '')
92
93
  }, 150)
@@ -149,12 +150,14 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
149
150
  const doc = /html/.test(res.type) ? new DOMParser().parseFromString(res.text, 'text/html') : null
150
151
  if (doc?.querySelector(NAV)?.getAttribute('data-brust-build') !== build) throw new Error(`not a brust page of this build`)
151
152
 
152
- // §3 step 4: everything the new page needs before it is shown. The store tags first, ahead of the old ones (a
153
- // store seeds from the first tag of its name; a live store never re-reads one), then the module scripts in
154
- // document order (one at a time: the order they register in is the order the server wrote), then the stylesheets
155
- // the head plan inserted, with `media="not all"`: loaded and parsed, applied to nothing until the swap.
153
+ // §3 step 4: everything the new page needs before it is shown. The store tags first, at the end of the body, after
154
+ // the old ones (a store seeds from the first tag of its name; a live store never re-reads one): an old-page island
155
+ // that hydrates while the modules load still finds its own page's tag, and a store new to this page, first read
156
+ // by one of its modules, finds the new one. The swap removes them with the old body tail. Then the module scripts
157
+ // in document order (one at a time: the order they register in is the order the server wrote), then the
158
+ // stylesheets the head plan inserted, with `media="not all"`: loaded and parsed, applied to nothing until the swap.
156
159
  const stores = Array.from(doc.querySelectorAll('script[data-brust-store]'), (s) => document.importNode(s, true))
157
- document.head.append(...stores)
160
+ document.body.append(...stores)
158
161
  added.push(...stores)
159
162
  for (const s of doc.querySelectorAll('script[type=module][src]')) {
160
163
  if (!s.matches(NAV)) await load(new URL(s.getAttribute('src')!, url).href)
@@ -186,8 +189,9 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
186
189
  shown = location.href
187
190
  land(url, pos.get(pop!))
188
191
  }
189
- // §10.3: the swap inside a view transition (a root cross-fade). Everything slow is done by now. The first 8 images of
190
- // the new page that are not lazy and not on screen already are preloaded (the same candidate: srcset, sizes,
192
+ // §10.3: the swap inside a view transition (a root cross-fade). Everything slow is done by now. The first 8 images the
193
+ // swap puts in (the changed level's, so a kept layout's never take a slot) that are not lazy and not on screen
194
+ // already are preloaded (the same candidate: srcset, sizes,
191
195
  // crossorigin and referrerpolicy copied) and get 100 ms in all to decode, so none pops in after the fade. `swap`
192
196
  // runs once: a throw from it rejects updateCallbackDone and takes the error path; a throw from startViewTransition
193
197
  // itself is a direct swap.
@@ -198,7 +202,7 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
198
202
  const onScreen = new Set([...document.querySelectorAll('img')].flatMap((i) => [i.src, i.currentSrc]).filter(Boolean))
199
203
  await Promise.race([
200
204
  new Promise((r) => setTimeout(r, 100)),
201
- Promise.all([...doc.querySelectorAll('img')]
205
+ Promise.all(incomingImages(doc)
202
206
  .filter((i) => i.getAttribute('loading') !== 'lazy' && (i.hasAttribute('src') || i.hasAttribute('srcset')) && !onScreen.has(abs(i.getAttribute('src'))))
203
207
  .slice(0, 8)
204
208
  .map((i) => {
package/src/nav/swap.ts CHANGED
@@ -95,6 +95,13 @@ function tail(body: HTMLElement): ChildNode[] | null {
95
95
  return tag && out
96
96
  }
97
97
 
98
+ /** The images `swapBody(doc)` will put in, before it runs: the changed level's, or the whole body's. */
99
+ export function incomingImages(doc: Document): HTMLImageElement[] {
100
+ const b = readChain(doc)
101
+ const i = swapLevel(readChain(document), b)
102
+ return all(i && tail(document.body) && tail(doc.body) ? between(b![i]!) : [...doc.body.childNodes], 'img') as HTMLImageElement[]
103
+ }
104
+
98
105
  /**
99
106
  * Swap the body to `doc`'s (§5.9): from the first level that changed, or the whole body (with the attributes of
100
107
  * `<html>` and `<body>`) when the root changed or a chain cannot be trusted. Returns the classic scripts of the new