@motion-proto/live-tokens 0.64.2 → 0.65.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.65.0 — A link the router cannot serve is the browser's
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The router hijacked links it could not serve.** Its click interception
|
|
8
|
+
claimed every anchor whose `href` began with `/`, reading neither `target`,
|
|
9
|
+
`download`, `rel`, nor whether any route rendered the path. So a link to a
|
|
10
|
+
file the origin serves — a PDF or an image under `public/`, a download, a
|
|
11
|
+
server endpoint — had its click cancelled and its path pushed at a router with
|
|
12
|
+
nothing to show for it, and the link silently did nothing. A left-click is now
|
|
13
|
+
claimed only when the anchor asks for ordinary same-tab navigation and a route
|
|
14
|
+
actually renders the path; everything else loads for real. Protocol-relative
|
|
15
|
+
hrefs (`//host/x`) are no longer mistaken for local paths either — they start
|
|
16
|
+
with `/`, and `pushState` throws on a cross-origin URL.
|
|
17
|
+
|
|
18
|
+
### Changed (breaking)
|
|
19
|
+
|
|
20
|
+
- **An unrouted path now performs a real navigation.** `pages['/']` is a
|
|
21
|
+
fallback for *rendering* an unmatched path, not a claim on it, so the router
|
|
22
|
+
no longer intercepts clicks on paths no route declares. On an SPA-rewriting
|
|
23
|
+
host the page still lands on the `pages['/']` fallback, now via a page load
|
|
24
|
+
rather than a client-side swap; on a host without the rewrite the URL 404s
|
|
25
|
+
instead of quietly rendering the home page. Return your own entry from
|
|
26
|
+
`resolve()` to keep claiming such paths.
|
|
27
|
+
|
|
28
|
+
## 0.64.3 — The add button names the act
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **The add button names the act, not the font.** It read `+ add Domine` — the
|
|
33
|
+
next family it would reach for — so the label changed under the pointer as a
|
|
34
|
+
stack filled up, and a control that renames itself between clicks is hard to
|
|
35
|
+
aim at. It now reads `+ add font`, and still falls back to `+ add fallback`
|
|
36
|
+
once every project font is in the stack.
|
|
37
|
+
|
|
3
38
|
## 0.64.2 — A stack can gain a second font
|
|
4
39
|
|
|
5
40
|
### Fixed
|
package/README.md
CHANGED
|
@@ -84,6 +84,8 @@ bootLiveTokens(App, '#app');
|
|
|
84
84
|
|
|
85
85
|
For routes you cannot enumerate ahead of time (a `/:id`, a path prefix, a page shown only under some condition), add a `resolve` function from the current path to a `RouteEntry` and return `null` to fall through. Resolution order is `pages[path]`, then `resolve(path)`, then the `pages['/']` fallback, so adding `resolve` never changes how existing entries match. A resolved entry can carry `props`, letting one component serve many paths, and its `source` gives the dynamic route a working "Page Source" button.
|
|
86
86
|
|
|
87
|
+
Link-click interception follows the same route table. A left-click becomes an in-app `navigate()` only when the anchor asks for ordinary same-tab navigation — no `target`, `download`, `rel="external"`, or modifier key — and `pages` or `resolve` claims the path. Anything else keeps the browser's own handling, so a link to a PDF or an image under `public/`, to a download, or to a path no route declares loads for real. Note that the `pages['/']` fallback renders an unmatched path without claiming it: link to a path no route declares and you get a page load, not a client-side swap.
|
|
88
|
+
|
|
87
89
|
```svelte
|
|
88
90
|
<LiveTokensRouter
|
|
89
91
|
pages={{ '/': { lazy: () => import('./Home.svelte'), label: 'Home' } }}
|
package/package.json
CHANGED
|
@@ -64,6 +64,67 @@
|
|
|
64
64
|
): RouteEntry | null {
|
|
65
65
|
return pages[route] ?? resolve?.(route) ?? pages['/'] ?? null;
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/** The click facts the interception decision reads, so it can be tested
|
|
69
|
+
without constructing a DOM MouseEvent. */
|
|
70
|
+
export interface LinkClick {
|
|
71
|
+
button: number;
|
|
72
|
+
ctrlKey: boolean;
|
|
73
|
+
metaKey: boolean;
|
|
74
|
+
shiftKey: boolean;
|
|
75
|
+
altKey: boolean;
|
|
76
|
+
defaultPrevented: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The path a left-click on `anchor` should navigate to in-app, or `null` to
|
|
81
|
+
* leave the click to the browser.
|
|
82
|
+
*
|
|
83
|
+
* Interception cancels the click, so every link this claims and cannot serve
|
|
84
|
+
* is a link that silently does nothing. It therefore claims a link only when
|
|
85
|
+
* the anchor asks for ordinary same-tab navigation *and* `claimsRoute` says a
|
|
86
|
+
* route renders that path — which leaves anything else the origin serves (a
|
|
87
|
+
* PDF or an image under `public/`, a download, a server-rendered endpoint) to
|
|
88
|
+
* load for real.
|
|
89
|
+
*
|
|
90
|
+
* `pages['/']` is a fallback for *rendering* an unmatched path, not a claim on
|
|
91
|
+
* it: treating it as one is what made every unrouted URL look interceptable.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveLinkNavigation(
|
|
94
|
+
anchor: Pick<Element, 'getAttribute' | 'hasAttribute'>,
|
|
95
|
+
e: LinkClick,
|
|
96
|
+
claimsRoute: (pathname: string) => boolean,
|
|
97
|
+
base: string,
|
|
98
|
+
): string | null {
|
|
99
|
+
if (e.defaultPrevented) return null;
|
|
100
|
+
// Middle- and right-clicks reach `auxclick`, not `click`, in current
|
|
101
|
+
// browsers; the guard costs nothing and keeps the contract explicit.
|
|
102
|
+
if (e.button !== 0) return null;
|
|
103
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return null;
|
|
104
|
+
|
|
105
|
+
const target = anchor.getAttribute('target');
|
|
106
|
+
if (target && target !== '_self') return null;
|
|
107
|
+
if (anchor.hasAttribute('download')) return null;
|
|
108
|
+
if (anchor.getAttribute('rel')?.split(/\s+/).includes('external')) return null;
|
|
109
|
+
|
|
110
|
+
const href = anchor.getAttribute('href');
|
|
111
|
+
// Relative and absolute-URL hrefs stay the browser's; only root-relative
|
|
112
|
+
// paths are candidates. `//host/x` starts with `/` and is cross-origin, and
|
|
113
|
+
// pushState would throw on it, so the origin check below has to run too.
|
|
114
|
+
if (!href?.startsWith('/')) return null;
|
|
115
|
+
|
|
116
|
+
let url: URL;
|
|
117
|
+
try {
|
|
118
|
+
url = new URL(href, base);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (url.origin !== new URL(base).origin) return null;
|
|
123
|
+
|
|
124
|
+
if (!claimsRoute(url.pathname)) return null;
|
|
125
|
+
|
|
126
|
+
return url.pathname + url.search + url.hash;
|
|
127
|
+
}
|
|
67
128
|
</script>
|
|
68
129
|
|
|
69
130
|
<script lang="ts">
|
|
@@ -176,17 +237,29 @@
|
|
|
176
237
|
return Promise.resolve({ default: null as unknown as Component<any, any, any> });
|
|
177
238
|
});
|
|
178
239
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
240
|
+
// Which paths this router can actually render. The `pages['/']` fallback in
|
|
241
|
+
// resolveRoute is deliberately not consulted: it renders an unmatched path,
|
|
242
|
+
// it does not claim one.
|
|
243
|
+
function claimsRoute(pathname: string): boolean {
|
|
244
|
+
if (pathname in pages) return true;
|
|
245
|
+
if (isDev && editorEnabled && pathname === editorPath) return true;
|
|
246
|
+
if (isDev && componentsEnabled && pathname === componentsPath) return true;
|
|
247
|
+
if (isDev && colorsEnabled && pathname === colorsPath) return true;
|
|
248
|
+
if (isDev && docsEnabled && pathname === docsPath) return true;
|
|
249
|
+
return !!resolve?.(pathname);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// In-app link interception: turn left-clicks on anchors this router can serve
|
|
253
|
+
// into navigate() calls, so route state updates without a full reload.
|
|
254
|
+
// Everything else — a new tab, a download, a file under `public/`, another
|
|
255
|
+
// origin — keeps the browser's own handling.
|
|
182
256
|
function handleClick(e: MouseEvent) {
|
|
183
|
-
const anchor = (e.target as
|
|
257
|
+
const anchor = (e.target as Element | null)?.closest?.('a[href]');
|
|
184
258
|
if (!anchor) return;
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
|
259
|
+
const path = resolveLinkNavigation(anchor, e, claimsRoute, window.location.href);
|
|
260
|
+
if (!path) return;
|
|
188
261
|
e.preventDefault();
|
|
189
|
-
navigate(
|
|
262
|
+
navigate(path);
|
|
190
263
|
}
|
|
191
264
|
</script>
|
|
192
265
|
|
|
@@ -193,8 +193,7 @@
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function addLabel(variable: FontStackVariable): string {
|
|
196
|
-
|
|
197
|
-
return next?.kind === 'project' ? `+ add ${slotDisplayName(next)}` : '+ add fallback';
|
|
196
|
+
return nextAddableSlot(variable)?.kind === 'project' ? '+ add font' : '+ add fallback';
|
|
198
197
|
}
|
|
199
198
|
|
|
200
199
|
/** A font joins the other fonts, above the fallbacks; a fallback lands just
|