@weft-js/start 0.1.4 → 0.1.5-nightly.20261111.3

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": "@weft-js/start",
3
- "version": "0.1.4",
3
+ "version": "0.1.5-nightly.20261111.3",
4
4
  "description": "weft Start: file routes, server functions, streaming SSR, a database and auth",
5
5
  "keywords": [
6
6
  "weft"
@@ -78,7 +78,7 @@
78
78
  "vite": "8.3.5"
79
79
  },
80
80
  "peerDependencies": {
81
- "@weft-js/core": "^0.1.4",
81
+ "@weft-js/core": "^0.1.5-nightly.20261111.3",
82
82
  "better-auth": "^1.7.0",
83
83
  "drizzle-orm": "^0.45.0",
84
84
  "drizzle-zod": "^0.8.0",
package/src/boot.ts CHANGED
@@ -8,6 +8,8 @@ export interface Held {
8
8
  p: (held: unknown) => void
9
9
  w?: (k: Element, lead?: 1) => void
10
10
  r?: 1
11
+ /** The page is running: a page that hydrates whole stops holding events off its islands. */
12
+ s?: 1
11
13
  n?: unknown
12
14
  l?: Element
13
15
  d?: unknown
package/src/capture.ts CHANGED
@@ -6,10 +6,12 @@
6
6
  //
7
7
  // q the events n the page needs its code at once w wake: (element, lead), a lead being only a hint of intent
8
8
  // p replay one r replaying, so don't capture d the visitor asked for less data l the last lead
9
+ // s the page is running: until then a page that hydrates whole (`n`) holds a click, submit or reset anywhere, not just
10
+ // on an island, since nothing is listening yet. A plain link is left to the browser: it works without code.
9
11
  //
10
12
  // Written by hand and small, since every such page pays it: `boot.ts` and weft's `island.ts` are the other two halves.
11
13
 
12
- const CODE = `(n=>{var d=document,c=navigator.connection,q=window.__wq={q:[],n:n,w(){},d:matchMedia("(prefers-reduced-data: reduce)").matches||!!(c&&c.saveData),p(e){var t=e.target,r=q.r,s=e.submitter;if(t.isConnected){q.r=1;try{e.type=="submit"?t.requestSubmit(s&&s.isConnected?s:void 0):e.type=="reset"?t.reset():t.dispatchEvent(new e.constructor(e.type,e))}finally{q.r=r}}}},o=t=>t.closest&&t.closest("[wk]"),h=e=>{var k=!q.r&&o(e.target);if(k&&(!k.$k||q.q.length)&&!((e.metaKey||e.ctrlKey||e.shiftKey)&&e.type=="click"&&e.target.closest("a"))){q.q.push(e);/^(click|submit|reset)$/.test(e.type)&&e.preventDefault();q.w(k)}},l=e=>{var k=o(e.target);k&&!k.$k&&!q.d&&q.w(k,1)};"click dblclick pointerdown pointerup mousedown mouseup keydown keyup input change submit reset focus blur focusin focusout".split(" ").forEach(t=>d.addEventListener(t,h,!0));"pointerover touchstart".split(" ").forEach(t=>d.addEventListener(t,l,{capture:!0,passive:!0}))})`
14
+ const CODE = `(n=>{var d=document,c=navigator.connection,q=window.__wq={q:[],n:n,w(){},d:matchMedia("(prefers-reduced-data: reduce)").matches||!!(c&&c.saveData),p(e){var t=e.target,r=q.r,s=e.submitter;if(t.isConnected){q.r=1;try{e.type=="submit"?t.requestSubmit(s&&s.isConnected?s:void 0):e.type=="reset"?t.reset():t.dispatchEvent(new e.constructor(e.type,e))}finally{q.r=r}}}},o=t=>t.closest&&t.closest("[wk]"),h=e=>{var t=e.target,k=!q.r&&(o(t)||q.n&&!q.s&&/^(click|submit|reset)$/.test(e.type)&&t.closest&&!t.closest("a[href]")&&d.documentElement);if(k&&(!k.$k||q.q.length)&&!((e.metaKey||e.ctrlKey||e.shiftKey)&&e.type=="click"&&e.target.closest("a"))){q.q.push(e);/^(click|submit|reset)$/.test(e.type)&&e.preventDefault();q.w(k)}},l=e=>{var k=o(e.target);k&&!k.$k&&!q.d&&q.w(k,1)};"click dblclick pointerdown pointerup mousedown mouseup keydown keyup input change submit reset focus blur focusin focusout".split(" ").forEach(t=>d.addEventListener(t,h,!0));"pointerover touchstart".split(" ").forEach(t=>d.addEventListener(t,l,{capture:!0,passive:!0}))})`
13
15
 
14
16
  /** The script's text. `now`: the page has work that can't wait, so its code loads straight away. */
15
17
  export const capture = (now: boolean) => `${CODE}(${now ? 1 : 0})`
@@ -35,46 +35,33 @@ export function admitLink(event: Event, routes: ClientRoutes) {
35
35
  }
36
36
  }
37
37
 
38
- /** Pointer admission and its click fallback; the returned disposer removes only these listeners. */
38
+ /**
39
+ * Links go on their click. A press (pointerdown) only warms the route: it starts the fetch the click will claim, so the
40
+ * head start is kept, but the page never changes under a pointer that is still down. Swapping it on the press lost the
41
+ * click itself (the button came up on the new page, so the click landed on <html> or never fired), and with it every
42
+ * click handler: a menu that closes when its link is followed, a sheet, an analytics hook. The returned disposer
43
+ * removes only these listeners.
44
+ */
39
45
  export function routeClicks(
40
46
  doc: Document,
41
47
  routes: ClientRoutes,
42
48
  go: (href: string) => void | Promise<void>,
49
+ warm?: (url: URL) => void,
43
50
  ) {
44
- let accepted: { id: number; anchor: HTMLAnchorElement } | undefined
45
51
  const down = (event: PointerEvent) => {
46
- accepted = undefined
47
52
  const target = admitLink(event, routes)
48
- if (!target) return
49
- accepted = { id: event.pointerId, anchor: target.anchor }
50
- if (!target.noop) void Promise.resolve(go(target.url.href)).catch(() => {})
51
- }
52
- const cancel = (event: PointerEvent) => {
53
- if (accepted?.id === event.pointerId) accepted = undefined
53
+ if (target && !target.noop) warm?.(target.url)
54
54
  }
55
55
  const click = (event: MouseEvent) => {
56
- const record = accepted
57
- accepted = undefined
58
- const pointer = event as PointerEvent
59
- if (
60
- record &&
61
- event.detail > 0 &&
62
- (pointer.pointerId === record.id || event.composedPath().includes(record.anchor))
63
- ) {
64
- event.preventDefault()
65
- return
66
- }
67
56
  const target = admitLink(event, routes)
68
57
  if (!target) return
69
58
  event.preventDefault()
70
- if (!target.noop) void go(target.url.href)
59
+ if (!target.noop) void Promise.resolve(go(target.url.href)).catch(() => {})
71
60
  }
72
61
  doc.addEventListener('pointerdown', down)
73
- doc.addEventListener('pointercancel', cancel)
74
62
  doc.addEventListener('click', click)
75
63
  return () => {
76
64
  doc.removeEventListener('pointerdown', down)
77
- doc.removeEventListener('pointercancel', cancel)
78
65
  doc.removeEventListener('click', click)
79
66
  }
80
67
  }
@@ -30,6 +30,7 @@ export function listeners(nav: Navigation, hot: () => Hot | undefined) {
30
30
  const q = (window as Window & { __wq?: Held }).__wq
31
31
  if (q) {
32
32
  q.w = wake
33
+ q.s = 1
33
34
  if (q.l) wake(q.l)
34
35
  drain()
35
36
  }
@@ -37,7 +38,17 @@ export function listeners(nav: Navigation, hot: () => Hot | undefined) {
37
38
 
38
39
  // A fade can intercept a click; replay it on the element underneath once the picture is gone.
39
40
  function clicks(nav: Navigation) {
40
- routeClicks(document, nav.routes, (href) => nav.go(href))
41
+ const network = (
42
+ navigator as Navigator & { connection?: { saveData?: boolean; effectiveType?: string } }
43
+ ).connection
44
+ routeClicks(
45
+ document,
46
+ nav.routes,
47
+ (href) => nav.go(href),
48
+ (url) => {
49
+ if (!document.hidden) nav.prefetch.start(url, 'press', network, navigator.onLine)
50
+ },
51
+ )
41
52
  let ended: Promise<unknown> | undefined
42
53
  document.addEventListener(
43
54
  'pointerdown',
@@ -123,7 +123,10 @@ export class Navigation {
123
123
  if (!current()) return
124
124
  if (scroll) {
125
125
  const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)))
126
- target ? target.scrollIntoView() : window.scrollTo(0, 0)
126
+ // a new page arrives where it starts, at once: warp makes the root scroll smooth for in-page anchors, and a
127
+ // route change that glided down from the old page's offset read as the old page scrolling away
128
+ if (target) target.scrollIntoView({ behavior: 'instant' })
129
+ else window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
127
130
  }
128
131
  }
129
132
  }
@@ -1,7 +1,8 @@
1
1
  import type { RouteCache, Read } from './cache'
2
2
  import { ask } from './helpers'
3
3
 
4
- type Intent = 'hover' | 'focus'
4
+ /** hover is a guess; focus and a press (the button is down, the click is coming) are near certain */
5
+ type Intent = 'hover' | 'focus' | 'press'
5
6
  type Network = { saveData?: boolean; effectiveType?: string }
6
7
  type Job = { ctrl: AbortController; promise: Promise<Read | undefined>; promoted: boolean }
7
8