@timber-js/app 0.1.57 → 0.2.0-alpha.2

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.
@@ -1,90 +0,0 @@
1
- /**
2
- * Link click interception and hover prefetch for SPA navigation.
3
- *
4
- * Handles click events on <a data-timber-link> and mouseenter events
5
- * on <a data-timber-prefetch> for client-side navigation.
6
- *
7
- * Extracted from browser-entry.ts to keep files under 500 lines.
8
- *
9
- * See design/19-client-navigation.md
10
- */
11
-
12
- import type { RouterInstance } from '@timber-js/app/client';
13
- import { ON_NAVIGATE_KEY } from './link-navigate-interceptor.js';
14
-
15
- // ─── Link Click Interception ─────────────────────────────────────
16
-
17
- /**
18
- * Handle click events on timber links. Intercepts clicks on <a> elements
19
- * marked with data-timber-link and triggers SPA navigation instead of
20
- * a full page load.
21
- *
22
- * Passes through to default browser behavior when:
23
- * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab
24
- * - The click is not the primary button
25
- * - The link has a target attribute (e.g., target="_blank")
26
- * - The link has a download attribute
27
- */
28
- export function handleLinkClick(event: MouseEvent, router: RouterInstance): void {
29
- // Only intercept primary clicks without modifier keys
30
- if (event.button !== 0) return;
31
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
32
- if (event.defaultPrevented) return;
33
-
34
- // Find the closest <a> ancestor with data-timber-link
35
- const anchor = (event.target as Element).closest?.(
36
- 'a[data-timber-link]'
37
- ) as HTMLAnchorElement | null;
38
- if (!anchor) return;
39
-
40
- // Don't intercept links that should open externally
41
- if (anchor.target && anchor.target !== '_self') return;
42
- if (anchor.hasAttribute('download')) return;
43
-
44
- const href = anchor.getAttribute('href');
45
- if (!href) return;
46
-
47
- // Prevent default navigation
48
- event.preventDefault();
49
-
50
- // Call onNavigate if registered on this anchor (via LinkNavigateInterceptor).
51
- // If the handler calls preventDefault(), skip the default SPA navigation —
52
- // the caller is responsible for navigating (e.g. via router.push()).
53
- const onNavigate = anchor[ON_NAVIGATE_KEY];
54
- if (onNavigate) {
55
- let prevented = false;
56
- onNavigate({
57
- preventDefault: () => {
58
- prevented = true;
59
- },
60
- });
61
- if (prevented) return;
62
- }
63
-
64
- // Check scroll preference from data attribute
65
- const scroll = anchor.getAttribute('data-timber-scroll') !== 'false';
66
-
67
- // Trigger SPA navigation
68
- void router.navigate(href, { scroll });
69
- }
70
-
71
- // ─── Prefetch on Hover ───────────────────────────────────────────
72
-
73
- /**
74
- * Handle mouseenter events on prefetch-enabled links. When the user
75
- * hovers over <a data-timber-prefetch>, the RSC payload is fetched
76
- * and cached for near-instant navigation.
77
- *
78
- * See design/19-client-navigation.md §"Prefetch Cache"
79
- */
80
- export function handleLinkHover(event: MouseEvent, router: RouterInstance): void {
81
- const anchor = (event.target as Element).closest?.(
82
- 'a[data-timber-prefetch]'
83
- ) as HTMLAnchorElement | null;
84
- if (!anchor) return;
85
-
86
- const href = anchor.getAttribute('href');
87
- if (!href) return;
88
-
89
- router.prefetch(href);
90
- }
@@ -1,62 +0,0 @@
1
- 'use client';
2
-
3
- // LinkNavigateInterceptor — client component that stores an onNavigate callback
4
- // on the parent <a> element so the delegated click handler in browser-entry.ts
5
- // can invoke it before triggering SPA navigation.
6
- //
7
- // See design/19-client-navigation.md, TIM-167
8
-
9
- import { useRef, useEffect, type ReactNode } from 'react';
10
-
11
- /** Symbol used to store the onNavigate callback on anchor elements. */
12
- export const ON_NAVIGATE_KEY = '__timberOnNavigate' as const;
13
-
14
- export type OnNavigateEvent = {
15
- preventDefault: () => void;
16
- };
17
-
18
- export type OnNavigateHandler = (e: OnNavigateEvent) => void;
19
-
20
- /**
21
- * Augment HTMLAnchorElement with the optional onNavigate property.
22
- * Used by browser-entry.ts handleLinkClick to check for the callback.
23
- */
24
- declare global {
25
- interface HTMLAnchorElement {
26
- [ON_NAVIGATE_KEY]?: OnNavigateHandler;
27
- }
28
- }
29
-
30
- /**
31
- * Client component rendered inside <Link> that attaches the onNavigate
32
- * callback to the closest <a> ancestor via a DOM property. The callback
33
- * is cleaned up on unmount.
34
- *
35
- * Renders no extra DOM — just a transparent wrapper.
36
- */
37
- export function LinkNavigateInterceptor({
38
- onNavigate,
39
- children,
40
- }: {
41
- onNavigate: OnNavigateHandler;
42
- children: ReactNode;
43
- }) {
44
- const ref = useRef<HTMLSpanElement>(null);
45
-
46
- useEffect(() => {
47
- const anchor = ref.current?.closest('a');
48
- if (!anchor) return;
49
- anchor[ON_NAVIGATE_KEY] = onNavigate;
50
- return () => {
51
- delete anchor[ON_NAVIGATE_KEY];
52
- };
53
- }, [onNavigate]);
54
-
55
- // Use a <span> with display:contents to avoid affecting layout.
56
- // The ref lets us walk up to the parent <a> in the effect.
57
- return (
58
- <span ref={ref} style={{ display: 'contents' }}>
59
- {children}
60
- </span>
61
- );
62
- }