mnfst 0.5.162 → 0.5.164
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/lib/manifest.appwrite.auth.js +107 -199
- package/lib/manifest.appwrite.data.js +1 -3
- package/lib/manifest.charts.js +88 -97
- package/lib/manifest.chat.js +515 -0
- package/lib/manifest.code.js +110 -340
- package/lib/manifest.colorpicker.js +252 -551
- package/lib/manifest.combobox.js +73 -91
- package/lib/manifest.components.js +34 -95
- package/lib/manifest.css +18 -144
- package/lib/manifest.data.js +189 -562
- package/lib/manifest.datepicker.js +43 -128
- package/lib/manifest.dropdowns.js +18 -39
- package/lib/manifest.export.js +29 -147
- package/lib/manifest.form.css +18 -12
- package/lib/manifest.icons.js +9 -24
- package/lib/manifest.integrity.json +23 -23
- package/lib/manifest.js +62 -126
- package/lib/manifest.localization.js +67 -200
- package/lib/manifest.markdown.js +65 -174
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +40 -139
- package/lib/manifest.router.js +24 -73
- package/lib/manifest.status.js +20 -56
- package/lib/manifest.svg.js +12 -30
- package/lib/manifest.tooltips.js +28 -65
- package/lib/manifest.utilities.js +40 -107
- package/lib/manifest.virtual.js +15 -53
- package/package.json +1 -1
- package/lib/manifest.appwrite.presence.js +0 -1650
package/lib/manifest.payments.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/* Manifest Payments
|
|
1
|
+
/* Manifest Payments — config
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
3
|
/* https://manifestx.dev
|
|
4
|
-
/*
|
|
5
|
-
/* Provider-agnostic payments surface (x-pay / $pay).
|
|
6
|
-
/* The client only ever talks to YOUR function endpoint — never a provider's
|
|
7
|
-
/* secret API directly. Session creation, fulfilment webhooks and any mutation
|
|
8
|
-
/* live server-side. See manifest.payments.core.js for the contract.
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Refuse strings still containing an unresolved ${VAR}. The loader interpolates
|
|
14
|
-
// against window.env before caching the manifest, so a literal ${VAR} here means
|
|
15
|
-
// an undefined env var — fail loud rather than POST it to the function verbatim.
|
|
6
|
+
// Refuse strings still containing an unresolved ${VAR} — fail loud rather than
|
|
7
|
+
// POST an undefined env var to the function verbatim.
|
|
16
8
|
function resolvedOrNull(value, fieldName) {
|
|
17
9
|
if (typeof value !== 'string') return value;
|
|
18
10
|
if (/\$\{[^}]+\}/.test(value)) {
|
|
@@ -35,13 +27,6 @@ async function ensureManifest() {
|
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
// Normalize manifest.payments into a stable config object.
|
|
38
|
-
// provider default adapter key (e.g. "revolut", "stripe", "paddle")
|
|
39
|
-
// endpoint YOUR function base — mints checkout/portal sessions, verifies webhooks
|
|
40
|
-
// mode default modality hint: "redirect" | "overlay" (server may override)
|
|
41
|
-
// publicKey publishable key for overlay SDK init (safe to expose; NOT a secret key)
|
|
42
|
-
// managed true = Manifest-hosted server moment (endpoint inferred)
|
|
43
|
-
// state optional reactive-state source for $pay.state: { url } (GET) — for
|
|
44
|
-
// managed mode. Appwrite-backed projects read state via $x instead.
|
|
45
30
|
let _cache = null;
|
|
46
31
|
async function getPaymentsConfig() {
|
|
47
32
|
if (_cache) return _cache;
|
|
@@ -71,33 +56,10 @@ async function getPaymentsConfig() {
|
|
|
71
56
|
window.ManifestPaymentsConfig = { getPaymentsConfig, ensureManifest };
|
|
72
57
|
|
|
73
58
|
|
|
74
|
-
/* Payments adapters */
|
|
75
|
-
//
|
|
76
|
-
// An adapter is the ONLY provider-specific client code. It knows how to open a
|
|
77
|
-
// provider's overlay/embedded checkout. Link-through (redirect) needs no adapter
|
|
78
|
-
// and is the universal floor — ANY provider with a hosted page works without one.
|
|
79
|
-
//
|
|
80
|
-
// Capability tiers:
|
|
81
|
-
// overlay (supportsOverlay: true) — provider exposes a programmatic modal
|
|
82
|
-
// with a success/cancel callback. Shipped
|
|
83
|
-
// for: revolut, paddle, lemonsqueezy,
|
|
84
|
-
// polar, razorpay.
|
|
85
|
-
// redirect (supportsOverlay: false) — no no-mount modal; works via the redirect
|
|
86
|
-
// floor (server returns {mode:'redirect',
|
|
87
|
-
// url}). Embedded/popup variants (Stripe
|
|
88
|
-
// Elements, Square Web Payments, PayPal
|
|
89
|
-
// Buttons, …) can be added per-provider via
|
|
90
|
-
// $pay.register(name, customAdapter).
|
|
91
|
-
//
|
|
92
|
-
// Adapter shape:
|
|
93
|
-
// { supportsOverlay: bool,
|
|
94
|
-
// async open({ url, params, config }) -> { status: 'complete'|'cancelled'|… } }
|
|
95
|
-
//
|
|
96
|
-
// The server (your function) decides modality per response, so adapters never see
|
|
97
|
-
// secret keys — only a hosted url, publishable params, or a public token.
|
|
59
|
+
/* Manifest Payments — provider adapters (overlay/embedded checkout) */
|
|
98
60
|
|
|
99
|
-
// Load a CDN script once; resolve when ready.
|
|
100
|
-
//
|
|
61
|
+
// Load a CDN script once; resolve when ready. Keeps heavy provider SDKs out of
|
|
62
|
+
// the bundle and prerendered output.
|
|
101
63
|
const _loaded = {};
|
|
102
64
|
function loadScript(src, attrs) {
|
|
103
65
|
if (_loaded[src]) return _loaded[src];
|
|
@@ -127,8 +89,7 @@ function list() { return Object.keys(_registry).map(n => ({ name: n, overlay: !!
|
|
|
127
89
|
|
|
128
90
|
/* ---- Overlay adapters (programmatic modal + success callback) ------------- */
|
|
129
91
|
|
|
130
|
-
// Revolut Merchant — popup over an order token.
|
|
131
|
-
// { provider:'revolut', params:{ token } } (token = the order's public id).
|
|
92
|
+
// Revolut Merchant — popup over an order token.
|
|
132
93
|
register('revolut', {
|
|
133
94
|
supportsOverlay: true,
|
|
134
95
|
async open({ params = {}, config }) {
|
|
@@ -148,8 +109,7 @@ register('revolut', {
|
|
|
148
109
|
}
|
|
149
110
|
});
|
|
150
111
|
|
|
151
|
-
// Paddle (MoR) — overlay via Paddle.js.
|
|
152
|
-
// { provider:'paddle', params:{ token, items, customer, … } } (token = publishable).
|
|
112
|
+
// Paddle (MoR) — overlay via Paddle.js.
|
|
153
113
|
register('paddle', {
|
|
154
114
|
supportsOverlay: true,
|
|
155
115
|
async open({ params = {}, config }) {
|
|
@@ -169,8 +129,7 @@ register('paddle', {
|
|
|
169
129
|
}
|
|
170
130
|
});
|
|
171
131
|
|
|
172
|
-
// Lemon Squeezy (MoR) — URL-in-a-modal via lemon.js.
|
|
173
|
-
// { provider:'lemonsqueezy', url } (a hosted checkout URL).
|
|
132
|
+
// Lemon Squeezy (MoR) — URL-in-a-modal via lemon.js.
|
|
174
133
|
register('lemonsqueezy', {
|
|
175
134
|
supportsOverlay: true,
|
|
176
135
|
async open({ url }) {
|
|
@@ -189,8 +148,7 @@ register('lemonsqueezy', {
|
|
|
189
148
|
}
|
|
190
149
|
});
|
|
191
150
|
|
|
192
|
-
// Polar (MoR) — URL-in-a-modal via the embed SDK.
|
|
193
|
-
// { provider:'polar', url } (a checkout-link/session url).
|
|
151
|
+
// Polar (MoR) — URL-in-a-modal via the embed SDK.
|
|
194
152
|
register('polar', {
|
|
195
153
|
supportsOverlay: true,
|
|
196
154
|
async open({ url }) {
|
|
@@ -206,8 +164,7 @@ register('polar', {
|
|
|
206
164
|
}
|
|
207
165
|
});
|
|
208
166
|
|
|
209
|
-
// Razorpay — true modal checkout.
|
|
210
|
-
// { provider:'razorpay', params:{ key, order_id, amount, … } }.
|
|
167
|
+
// Razorpay — true modal checkout.
|
|
211
168
|
register('razorpay', {
|
|
212
169
|
supportsOverlay: true,
|
|
213
170
|
async open({ params = {} }) {
|
|
@@ -225,34 +182,15 @@ register('razorpay', {
|
|
|
225
182
|
});
|
|
226
183
|
|
|
227
184
|
/* ---- Redirect-floor providers --------------------------------------------- */
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// checkout / payment link / order approve url). Registered for discoverability and
|
|
231
|
-
// so a mistaken mode:'overlay' degrades cleanly. Add an embedded/popup variant any
|
|
232
|
-
// time with $pay.register(name, { supportsOverlay:true, open }).
|
|
233
|
-
//
|
|
234
|
-
// stripe Checkout session url (Elements/embedded = custom adapter)
|
|
235
|
-
// square Payment Link / Checkout API url (Web Payments SDK = custom)
|
|
236
|
-
// paypal Orders API approve url (Smart Buttons popup = custom)
|
|
237
|
-
// braintree hosted / Drop-in (Drop-in needs a mount node = custom)
|
|
238
|
-
// adyen Pay-by-Link / hosted (Drop-in/Components = custom)
|
|
239
|
-
// mollie hosted checkout (redirect-first by design)
|
|
240
|
-
//
|
|
241
|
-
// NOTE: any provider NOT listed here still works with zero config via the same
|
|
242
|
-
// redirect floor (absolute-URL ref, or the function returning {mode:'redirect'}).
|
|
243
|
-
// Hosted destinations like Patreon / Buy Me a Coffee / Ko-fi / donation pages are
|
|
244
|
-
// just link-through: <a x-pay="'https://patreon.com/you'">…</a>. No adapter needed.
|
|
185
|
+
// Registered for discoverability; no overlay, so they degrade to the redirect
|
|
186
|
+
// floor. Unlisted providers work the same way with zero config.
|
|
245
187
|
['stripe', 'square', 'paypal', 'braintree', 'adyen', 'mollie']
|
|
246
188
|
.forEach((name) => register(name, { supportsOverlay: false, redirectOnly: true }));
|
|
247
189
|
|
|
248
190
|
window.ManifestPaymentsAdapters = { register, get, list, loadScript };
|
|
249
191
|
|
|
250
192
|
|
|
251
|
-
/* Payments store */
|
|
252
|
-
//
|
|
253
|
-
// Reactive in-flight + server-defined state for $pay. Commerce semantics are NOT
|
|
254
|
-
// modelled here — `state` is whatever your function returns (schema-less). The
|
|
255
|
-
// core engine (plain JS) mutates these fields via Alpine.store('pay').
|
|
193
|
+
/* Manifest Payments — reactive store for $pay */
|
|
256
194
|
|
|
257
195
|
function initializePaymentsStore() {
|
|
258
196
|
if (typeof Alpine === 'undefined') return;
|
|
@@ -274,7 +212,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
274
212
|
window.ManifestPaymentsStore = { initialize: initializePaymentsStore };
|
|
275
213
|
|
|
276
214
|
|
|
277
|
-
/* Payments core */
|
|
215
|
+
/* Manifest Payments — core engine (client ↔ function contract) */
|
|
278
216
|
//
|
|
279
217
|
// CONTRACT (client → your function):
|
|
280
218
|
// POST {endpoint} { ref, payload, context }
|
|
@@ -283,24 +221,19 @@ window.ManifestPaymentsStore = { initialize: initializePaymentsStore };
|
|
|
283
221
|
// → { mode:'overlay', provider, url } URL-in-a-modal overlay
|
|
284
222
|
// GET {state.url}?workspace=…&user=… → arbitrary entitlement record
|
|
285
223
|
//
|
|
286
|
-
// `ref` is
|
|
287
|
-
//
|
|
288
|
-
// secret keys and webhook fulfilment live behind the function. Fulfilment is the
|
|
289
|
-
// webhook — never trust this redirect/callback; always reconcile against state.
|
|
224
|
+
// `ref` is opaque — Manifest never interprets it. Fulfilment is the webhook, not
|
|
225
|
+
// this redirect/callback; always reconcile against state.
|
|
290
226
|
|
|
291
227
|
const RETURN_PARAM = 'checkout';
|
|
292
228
|
|
|
293
229
|
function store() { return window.Alpine?.store('pay') || null; }
|
|
294
230
|
function setStore(patch) { const s = store(); if (s) Object.assign(s, patch); }
|
|
295
231
|
|
|
296
|
-
// Navigation indirection
|
|
297
|
-
// setNavigate() to intercept (SPA router integration, or test harnesses that
|
|
298
|
-
// must not actually leave the page).
|
|
232
|
+
// Navigation indirection; override via setNavigate() for SPA routers or tests.
|
|
299
233
|
let _navigate = (url) => window.location.assign(url);
|
|
300
234
|
function setNavigate(fn) { if (typeof fn === 'function') _navigate = fn; }
|
|
301
235
|
|
|
302
|
-
// Identity context, auto-injected
|
|
303
|
-
// Read from the auth store if the auth plugin is present; absent otherwise.
|
|
236
|
+
// Identity context, auto-injected from the auth store when present.
|
|
304
237
|
function getContext() {
|
|
305
238
|
const ctx = {};
|
|
306
239
|
try {
|
|
@@ -323,9 +256,7 @@ async function postSession(config, ref, payload, preferMode) {
|
|
|
323
256
|
body: JSON.stringify({ ref, payload: payload || null, context })
|
|
324
257
|
});
|
|
325
258
|
if (!res.ok) {
|
|
326
|
-
// Surface the function's own
|
|
327
|
-
// { message } strings meant for the shopper) rather than a bare status
|
|
328
|
-
// code. Fall back to a friendly generic if the body isn't readable JSON.
|
|
259
|
+
// Surface the function's own { error }/{ message } over a bare status code.
|
|
329
260
|
let message = '';
|
|
330
261
|
try {
|
|
331
262
|
const body = await res.json();
|
|
@@ -336,8 +267,8 @@ async function postSession(config, ref, payload, preferMode) {
|
|
|
336
267
|
return res.json();
|
|
337
268
|
}
|
|
338
269
|
|
|
339
|
-
// Drive a server response into the right modality
|
|
340
|
-
// when no adapter supports it
|
|
270
|
+
// Drive a server response into the right modality; overlay degrades to redirect
|
|
271
|
+
// when no adapter supports it.
|
|
341
272
|
async function dispatch(response, config) {
|
|
342
273
|
const mode = response?.mode || 'redirect';
|
|
343
274
|
if (mode === 'overlay') {
|
|
@@ -357,10 +288,8 @@ async function dispatch(response, config) {
|
|
|
357
288
|
return { status: 'redirected' };
|
|
358
289
|
}
|
|
359
290
|
|
|
360
|
-
// Public: initiate a payment flow for an opaque ref
|
|
361
|
-
//
|
|
362
|
-
// link-through with no server (the "embed a checkout link today" case).
|
|
363
|
-
// payload optional opaque data forwarded to the function (+ optional .context).
|
|
291
|
+
// Public: initiate a payment flow for an opaque ref (or an absolute URL for a
|
|
292
|
+
// zero-server link-through).
|
|
364
293
|
async function initiate(ref, payload = {}) {
|
|
365
294
|
setStore({ loading: true, error: null });
|
|
366
295
|
try {
|
|
@@ -386,12 +315,10 @@ async function initiate(ref, payload = {}) {
|
|
|
386
315
|
}
|
|
387
316
|
}
|
|
388
317
|
|
|
389
|
-
// Convenience: open the billing/customer portal
|
|
390
|
-
// default) the function maps to a server-minted portal session.
|
|
318
|
+
// Convenience: open the billing/customer portal (just another ref).
|
|
391
319
|
function portal(ref = 'portal', payload = {}) { return initiate(ref, payload); }
|
|
392
320
|
|
|
393
|
-
// Read server-defined entitlement state into $pay.state (managed mode).
|
|
394
|
-
// projects should read state reactively via $x instead of configuring state.url.
|
|
321
|
+
// Read server-defined entitlement state into $pay.state (managed mode).
|
|
395
322
|
async function refreshState() {
|
|
396
323
|
const config = await window.ManifestPaymentsConfig.getPaymentsConfig();
|
|
397
324
|
if (!config?.state?.url) return null;
|
|
@@ -411,15 +338,9 @@ async function refreshState() {
|
|
|
411
338
|
}
|
|
412
339
|
}
|
|
413
340
|
|
|
414
|
-
// On return from a redirect checkout
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// function's success_url) so we know to reconcile.
|
|
418
|
-
//
|
|
419
|
-
// The provider's webhook can land seconds AFTER the buyer does, so a single
|
|
420
|
-
// refresh can read pre-payment state. Re-poll on a short backoff to absorb the
|
|
421
|
-
// lag — each refresh overwrites $pay.state, so the page settles on the granted
|
|
422
|
-
// record without author code.
|
|
341
|
+
// On return from a redirect checkout (success_url carries ?checkout=…), re-pull
|
|
342
|
+
// state and strip the marker. The webhook can land seconds after the buyer, so
|
|
343
|
+
// re-poll on a backoff to absorb the lag; each refresh overwrites $pay.state.
|
|
423
344
|
const RETURN_POLL_MS = [2000, 5000, 10000];
|
|
424
345
|
function handleReturn() {
|
|
425
346
|
try {
|
|
@@ -439,19 +360,13 @@ function handleReturn() {
|
|
|
439
360
|
window.ManifestPayments = { initiate, portal, refreshState, handleReturn, getContext, setNavigate };
|
|
440
361
|
|
|
441
362
|
|
|
442
|
-
/* Payments magic (
|
|
363
|
+
/* Manifest Payments — $pay magic (callable + reactive props) */
|
|
443
364
|
//
|
|
444
|
-
//
|
|
445
|
-
// $pay('pro-monthly') initiate a flow for an opaque ref → Promise
|
|
446
|
-
// $pay('https://buy.…') absolute URL → plain link-through, no server
|
|
365
|
+
// $pay(ref|url) initiate a flow → Promise
|
|
447
366
|
// $pay.portal() open the billing/customer portal
|
|
448
367
|
// $pay.refresh() re-pull server state into $pay.state
|
|
449
|
-
// $pay.register(name, adapter) add/override an overlay adapter
|
|
450
|
-
// $pay.state
|
|
451
|
-
// $pay.loading / $pay.error / $pay.last
|
|
452
|
-
//
|
|
453
|
-
// Reads of .state/.loading/etc. go through the reactive 'pay' store, so Alpine
|
|
454
|
-
// tracks them in x-show / x-text. State for Appwrite projects is read via $x.
|
|
368
|
+
// $pay.register(name, adapter) add/override an overlay adapter
|
|
369
|
+
// $pay.state / .loading / .error / .last reactive (tracked in x-show/x-text)
|
|
455
370
|
|
|
456
371
|
function initializePaymentsMagic() {
|
|
457
372
|
if (typeof Alpine === 'undefined') return;
|
|
@@ -481,19 +396,11 @@ document.addEventListener('alpine:init', () => {
|
|
|
481
396
|
window.ManifestPaymentsMagic = { initialize: initializePaymentsMagic };
|
|
482
397
|
|
|
483
398
|
|
|
484
|
-
/* Payments
|
|
485
|
-
//
|
|
486
|
-
// Sugar for "on click, initiate this ref". Markup is provider- AND modality-
|
|
487
|
-
// agnostic — switching either is a config/server change, never an HTML change.
|
|
488
|
-
// <button x-pay="'pro-monthly'">Subscribe</button>
|
|
489
|
-
// <button x-pay="'credits-1000'">Buy credits</button>
|
|
490
|
-
// <button x-pay.portal>Manage billing</button>
|
|
491
|
-
// <button x-pay.overlay="'pro-monthly'">Subscribe</button> modality hint
|
|
492
|
-
// <a x-pay="cart.token">Checkout</a>
|
|
399
|
+
/* Manifest Payments — x-pay directive (click → initiate a ref) */
|
|
493
400
|
//
|
|
494
401
|
// Modifiers:
|
|
495
|
-
// .portal
|
|
496
|
-
// .overlay/.redirect hint preferred modality
|
|
402
|
+
// .portal portal flow (ref defaults to "portal")
|
|
403
|
+
// .overlay/.redirect hint preferred modality (server decides)
|
|
497
404
|
|
|
498
405
|
function initializePaymentsDirective() {
|
|
499
406
|
if (typeof Alpine === 'undefined') return;
|
|
@@ -508,11 +415,9 @@ function initializePaymentsDirective() {
|
|
|
508
415
|
const hasExpr = expression && expression.trim().length > 0;
|
|
509
416
|
const getRef = hasExpr ? evaluateLater(expression) : null;
|
|
510
417
|
|
|
511
|
-
// Busy guard: ignore clicks
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
// when it's loaded ($toast resolved through this element's Alpine
|
|
515
|
-
// scope); programmatic $pay() callers handle their own rejections.
|
|
418
|
+
// Busy guard: ignore clicks and disable the element while in flight, so
|
|
419
|
+
// double-clicks can't mint duplicate sessions. On failure, surface a
|
|
420
|
+
// toast if the toasts plugin is loaded.
|
|
516
421
|
const run = async (ref) => {
|
|
517
422
|
if (el.getAttribute('aria-busy') === 'true') return;
|
|
518
423
|
const payload = modeHint ? { mode: modeHint } : {};
|
|
@@ -556,11 +461,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
556
461
|
window.ManifestPaymentsDirective = { initialize: initializePaymentsDirective };
|
|
557
462
|
|
|
558
463
|
|
|
559
|
-
/* Payments
|
|
560
|
-
//
|
|
561
|
-
// Orchestration: settle redirect-returns and pull initial state once Alpine and
|
|
562
|
-
// (optionally) auth are ready. Registration of store/magic/directive happens in
|
|
563
|
-
// their own subscripts on alpine:init.
|
|
464
|
+
/* Manifest Payments — boot (settle redirect-returns, pull initial state) */
|
|
564
465
|
|
|
565
466
|
function bootPayments() {
|
|
566
467
|
if (window.__manifestPaymentsBooted) return;
|
package/lib/manifest.router.js
CHANGED
|
@@ -124,12 +124,11 @@ let currentRoute = '/';
|
|
|
124
124
|
let isInternalNavigation = false;
|
|
125
125
|
|
|
126
126
|
function isPrerenderedStaticBuild() {
|
|
127
|
-
//
|
|
128
|
-
// so each URL loads its own prerendered HTML rather than SPA toggling.
|
|
127
|
+
// Prerendered static pages navigate MPA-style (each URL loads its own HTML).
|
|
129
128
|
const prerendered = document.querySelector('meta[name="manifest:prerendered"]');
|
|
130
129
|
const val = (prerendered?.getAttribute('content') || '').trim().toLowerCase();
|
|
131
130
|
if (prerendered && val !== '0' && val !== 'false') return true;
|
|
132
|
-
// Backstop:
|
|
131
|
+
// Backstop: per-page depth marker written by prerender.
|
|
133
132
|
return !!document.querySelector('meta[name="manifest:router-base-depth"]');
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -326,17 +325,12 @@ async function handleRouteChange() {
|
|
|
326
325
|
const prevRoute = currentRoute;
|
|
327
326
|
currentRoute = newRoute;
|
|
328
327
|
|
|
329
|
-
//
|
|
328
|
+
// Route change scrolls to top; anchor links let the browser scroll naturally.
|
|
330
329
|
if (!window.location.hash) {
|
|
331
|
-
// This is a route change - scroll to top
|
|
332
|
-
// Use a small delay to ensure content has loaded
|
|
333
330
|
setTimeout(() => {
|
|
334
|
-
// Scroll main page to top
|
|
335
331
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
336
332
|
|
|
337
|
-
//
|
|
338
|
-
// Use a generic approach that works with any CSS framework
|
|
339
|
-
// Only check elements that are likely to be scrollable containers
|
|
333
|
+
// Also reset any scrollable containers.
|
|
340
334
|
const potentialContainers = document.querySelectorAll('div, main, section, article, aside, nav, header, footer, .prose');
|
|
341
335
|
potentialContainers.forEach(element => {
|
|
342
336
|
const computedStyle = window.getComputedStyle(element);
|
|
@@ -353,11 +347,8 @@ async function handleRouteChange() {
|
|
|
353
347
|
});
|
|
354
348
|
}, 50);
|
|
355
349
|
} else {
|
|
356
|
-
// This is an anchor link - let the browser handle the scroll naturally
|
|
357
|
-
// Use a small delay to ensure content has loaded, then let browser scroll to anchor
|
|
358
350
|
setTimeout(() => {
|
|
359
|
-
//
|
|
360
|
-
// We just need to ensure the content is loaded first
|
|
351
|
+
// Let the browser scroll to the anchor once content has loaded.
|
|
361
352
|
}, 50);
|
|
362
353
|
}
|
|
363
354
|
|
|
@@ -370,18 +361,9 @@ async function handleRouteChange() {
|
|
|
370
361
|
}
|
|
371
362
|
});
|
|
372
363
|
|
|
373
|
-
// SPA
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// `@view-transition { navigation: auto }` in the framework's reset CSS;
|
|
377
|
-
// the same `::view-transition-group(*)` rule (driven by
|
|
378
|
-
// `--view-transition-duration` / `--view-transition-easing`) covers both.
|
|
379
|
-
//
|
|
380
|
-
// The callback is synchronous: visibility/head/anchor listeners mutate
|
|
381
|
-
// the DOM inside `dispatchEvent` and return. Returning anything async
|
|
382
|
-
// here would freeze the rendered frame until the promise resolves,
|
|
383
|
-
// adding the entirety of Alpine's pending-update queue to the perceived
|
|
384
|
-
// navigation time (1–2s on busy pages).
|
|
364
|
+
// Wrap the SPA dispatch in a View Transition when available (MPA is handled
|
|
365
|
+
// by @view-transition in reset CSS). Callback must stay synchronous:
|
|
366
|
+
// returning a promise freezes the frame until Alpine's update queue drains.
|
|
385
367
|
if (shouldUseViewTransition()) {
|
|
386
368
|
document.startViewTransition(() => {
|
|
387
369
|
window.dispatchEvent(event);
|
|
@@ -391,30 +373,12 @@ async function handleRouteChange() {
|
|
|
391
373
|
}
|
|
392
374
|
}
|
|
393
375
|
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// under VT_AUTO_THRESHOLD elements,
|
|
401
|
-
// OFF otherwise
|
|
402
|
-
//
|
|
403
|
-
// The auto threshold exists because the View Transitions API rasterizes the
|
|
404
|
-
// full viewport for the "before" and "after" snapshots; cost scales linearly
|
|
405
|
-
// with DOM size and gets noticeable above a few thousand elements (a 10k-
|
|
406
|
-
// element page measured ~500ms per snapshot in dev). Light pages keep the
|
|
407
|
-
// crossfade; heavy pages stay fast.
|
|
408
|
-
//
|
|
409
|
-
// Cross-document (MPA) navigations are unaffected — those use the browser's
|
|
410
|
-
// native cross-document path (`@view-transition { navigation: auto }`),
|
|
411
|
-
// which rasterizes in parallel with page load and doesn't expose the cost.
|
|
412
|
-
//
|
|
413
|
-
// Per-element opt-out (`data-no-view-transition`, singular) on individual
|
|
414
|
-
// elements is handled by the existing reset CSS rule that sets
|
|
415
|
-
// `view-transition-name: none` on them. `prefers-reduced-motion` is
|
|
416
|
-
// respected automatically — the browser falls back to a snap with no
|
|
417
|
-
// animation when the user has it set.
|
|
376
|
+
// Whether SPA route changes run inside a View Transition. Priority:
|
|
377
|
+
// data-no-view-transitions → off, data-view-transitions → on, else auto
|
|
378
|
+
// (on under VT_AUTO_THRESHOLD elements). Auto threshold: VT rasterizes the
|
|
379
|
+
// full viewport per snapshot, so cost scales with DOM size (~500ms/snapshot
|
|
380
|
+
// on a 10k-element page). Per-element opt-out and prefers-reduced-motion are
|
|
381
|
+
// handled in reset CSS / by the browser.
|
|
418
382
|
const VT_AUTO_THRESHOLD = 3000;
|
|
419
383
|
|
|
420
384
|
function shouldUseViewTransition() {
|
|
@@ -431,14 +395,9 @@ function shouldUseViewTransition() {
|
|
|
431
395
|
}
|
|
432
396
|
}
|
|
433
397
|
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
// when DevTools is open, when launched via `open <url>`, or on mobile — so
|
|
438
|
-
// this check exclusively affects automation tooling and leaves end-user
|
|
439
|
-
// behavior identical. Authors who want transitions in their automation tests
|
|
440
|
-
// can force them on with `<html data-view-transitions>`, which takes priority
|
|
441
|
-
// over this attribute via `shouldUseViewTransition()` above.
|
|
398
|
+
// Disable transitions under WebDriver automation, which captures screenshots
|
|
399
|
+
// mid-transition and gets blank frames. Only affects automation
|
|
400
|
+
// (navigator.webdriver); data-view-transitions overrides.
|
|
442
401
|
if (typeof navigator !== 'undefined' && navigator.webdriver === true) {
|
|
443
402
|
const html = document.documentElement;
|
|
444
403
|
if (html && !html.hasAttribute('data-view-transitions')) {
|
|
@@ -514,9 +473,8 @@ function installMpaStickyLocaleLinks() {
|
|
|
514
473
|
event.preventDefault();
|
|
515
474
|
event.stopPropagation();
|
|
516
475
|
event.stopImmediatePropagation();
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
// falling back to the root index.html.
|
|
476
|
+
// MPA directory paths need a trailing slash so the static host resolves
|
|
477
|
+
// them to the right index.html, not the root one.
|
|
520
478
|
const hasFileExt = /\.[a-zA-Z0-9]+$/.test(adjusted);
|
|
521
479
|
url.pathname = (adjusted !== '/' && !hasFileExt && !adjusted.endsWith('/'))
|
|
522
480
|
? adjusted + '/'
|
|
@@ -547,18 +505,11 @@ function interceptLinkClicks() {
|
|
|
547
505
|
// Handle pure anchor links normally - don't intercept them
|
|
548
506
|
if (href.startsWith('#')) return;
|
|
549
507
|
|
|
550
|
-
// Don't intercept blob
|
|
551
|
-
//
|
|
552
|
-
// clicks it to trigger a file save; if the router treats it as a
|
|
553
|
-
// SPA link, `new URL("blob:http://localhost/UUID", origin).pathname`
|
|
554
|
-
// resolves to "http://localhost/UUID" which then pushState pushes
|
|
555
|
-
// as a same-origin path, producing /http://localhost/UUID and
|
|
556
|
-
// landing the user on a 404 instead of downloading.
|
|
508
|
+
// Don't intercept blob:/data: (e.g. export plugin's download links) —
|
|
509
|
+
// pushState would turn them into a bogus same-origin path and 404.
|
|
557
510
|
if (href.startsWith('blob:') || href.startsWith('data:')) return;
|
|
558
511
|
|
|
559
|
-
// Honor `download` —
|
|
560
|
-
// favor of letting the browser save its target. Same intent as
|
|
561
|
-
// blob:/data:, just expressed via the standard HTML attribute.
|
|
512
|
+
// Honor `download` — same opt-out intent, via the standard attribute.
|
|
562
513
|
if (link.hasAttribute('download')) return;
|
|
563
514
|
|
|
564
515
|
// Check if it's an external link FIRST (before any other processing)
|
|
@@ -651,8 +602,8 @@ function initializeNavigation() {
|
|
|
651
602
|
handleRouteChange();
|
|
652
603
|
}
|
|
653
604
|
|
|
654
|
-
// Match the browser URL
|
|
655
|
-
//
|
|
605
|
+
// Match the browser URL at module load: later chunks (e.g. router magic) may
|
|
606
|
+
// read getCurrentRoute() before DOMContentLoaded; stale '/' breaks $route.
|
|
656
607
|
currentRoute = pathnameToLogical(window.location.pathname);
|
|
657
608
|
|
|
658
609
|
// Run immediately if DOM is ready, otherwise wait
|