mnfst 0.5.79 → 0.5.81
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 +66 -33
- package/lib/manifest.code.css +19 -7
- package/lib/manifest.code.min.css +1 -1
- package/lib/manifest.components.js +25 -155
- package/lib/manifest.css +47 -45
- package/lib/manifest.data.js +42 -1
- package/lib/manifest.integrity.json +26 -0
- package/lib/manifest.js +60 -5
- package/lib/manifest.markdown.js +95 -3
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.router.js +49 -76
- package/lib/manifest.schema.json +1 -1
- package/lib/manifest.sidebar.css +3 -3
- package/lib/manifest.svg.js +68 -5
- package/lib/manifest.typography.css +37 -41
- package/lib/manifest.utilities.css +7 -1
- package/lib/manifest.utilities.js +9 -29
- package/package.json +4 -7
- package/lib/manifest.export.js +0 -509
- package/lib/manifest.virtual.js +0 -319
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
/* Auth config */
|
|
10
10
|
|
|
11
|
+
// Refuse strings that still contain an unresolved ${VAR} reference. The loader
|
|
12
|
+
// runs window.ManifestDataConfig.interpolateManifest at manifest-load time, so
|
|
13
|
+
// by the time we read these fields the env-var substitution has already been
|
|
14
|
+
// applied. Anything still matching ${VAR} is an undefined env var — passing it
|
|
15
|
+
// to Appwrite would either silently fail or, worse, be sent verbatim as an
|
|
16
|
+
// HTTP header value, leaking the env var name. Loud-fail instead.
|
|
17
|
+
function resolvedOrNull(value, fieldName) {
|
|
18
|
+
if (typeof value !== 'string') return value;
|
|
19
|
+
if (/\$\{[^}]+\}/.test(value)) {
|
|
20
|
+
console.error(`[Manifest Auth] manifest.appwrite.${fieldName} references an undefined env var (${value}). Auth disabled.`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
// Load manifest if not already loaded (loader may set __manifestLoaded / registry.manifest)
|
|
12
27
|
async function ensureManifest() {
|
|
13
28
|
if (window.ManifestComponentsRegistry?.manifest) {
|
|
@@ -34,13 +49,22 @@ async function getAppwriteConfig() {
|
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
const appwriteConfig = manifest.appwrite;
|
|
37
|
-
const endpoint = appwriteConfig.endpoint;
|
|
38
|
-
const projectId = appwriteConfig.projectId;
|
|
39
|
-
|
|
52
|
+
const endpoint = resolvedOrNull(appwriteConfig.endpoint, 'endpoint');
|
|
53
|
+
const projectId = resolvedOrNull(appwriteConfig.projectId, 'projectId');
|
|
54
|
+
// Optional dev key to bypass rate limits in development. The schema
|
|
55
|
+
// documents `${VAR_NAME}` interpolation for this field specifically —
|
|
56
|
+
// refuse to forward a literal placeholder as an HTTP header.
|
|
57
|
+
const devKey = appwriteConfig.devKey ? resolvedOrNull(appwriteConfig.devKey, 'devKey') : undefined;
|
|
40
58
|
|
|
41
59
|
if (!endpoint || !projectId) {
|
|
42
60
|
return null;
|
|
43
61
|
}
|
|
62
|
+
// devKey is optional: if the user supplied one but it failed to resolve,
|
|
63
|
+
// resolvedOrNull returned null (and logged) — drop the field rather than
|
|
64
|
+
// initialize Appwrite with a literal `${VAR}` header.
|
|
65
|
+
if (appwriteConfig.devKey && devKey === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
44
68
|
|
|
45
69
|
// Get auth methods from config (defaults to ["magic", "oauth"] if not specified)
|
|
46
70
|
const authMethods = appwriteConfig.auth?.methods || ["magic", "oauth"];
|
|
@@ -164,6 +188,27 @@ function initializeAuthStore() {
|
|
|
164
188
|
// Cross-tab synchronization using localStorage events
|
|
165
189
|
const STORAGE_KEY = 'manifest:auth:state';
|
|
166
190
|
|
|
191
|
+
// Whitelist of Appwrite Session fields safe to mirror across tabs.
|
|
192
|
+
// CRITICALLY excludes `secret` (the bearer credential), `providerAccessToken`,
|
|
193
|
+
// `providerRefreshToken`, and `providerAccessTokenExpiry`. The cookie set
|
|
194
|
+
// on the Appwrite domain is the actual auth of record; this localStorage
|
|
195
|
+
// copy only supports UI cross-tab sync ("someone just logged in here").
|
|
196
|
+
// An XSS on this origin must not be able to lift session secrets out.
|
|
197
|
+
const SAFE_SESSION_FIELDS = [
|
|
198
|
+
'$id', 'userId', 'provider', 'expire', 'current',
|
|
199
|
+
'clientName', 'osName', 'osCode', 'deviceName',
|
|
200
|
+
'deviceBrand', 'deviceModel', 'countryCode', 'countryName'
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
function sanitizeSessionForStorage(session) {
|
|
204
|
+
if (!session || typeof session !== 'object') return session;
|
|
205
|
+
const safe = {};
|
|
206
|
+
for (const f of SAFE_SESSION_FIELDS) {
|
|
207
|
+
if (f in session) safe[f] = session[f];
|
|
208
|
+
}
|
|
209
|
+
return safe;
|
|
210
|
+
}
|
|
211
|
+
|
|
167
212
|
// Listen for storage events from other tabs
|
|
168
213
|
window.addEventListener('storage', (e) => {
|
|
169
214
|
if (e.key === STORAGE_KEY && e.newValue) {
|
|
@@ -193,7 +238,7 @@ function initializeAuthStore() {
|
|
|
193
238
|
isAuthenticated: store.isAuthenticated,
|
|
194
239
|
isAnonymous: store.isAnonymous,
|
|
195
240
|
user: store.user,
|
|
196
|
-
session: store.session,
|
|
241
|
+
session: sanitizeSessionForStorage(store.session),
|
|
197
242
|
magicLinkSent: store.magicLinkSent,
|
|
198
243
|
magicLinkExpired: store.magicLinkExpired,
|
|
199
244
|
error: store.error
|
|
@@ -5769,13 +5814,11 @@ function initializeMagicLinks() {
|
|
|
5769
5814
|
);
|
|
5770
5815
|
|
|
5771
5816
|
if (isRateLimit) {
|
|
5772
|
-
//
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
}
|
|
5778
|
-
this.error = 'Rate limit exceeded. Please wait a moment and refresh the page.';
|
|
5817
|
+
// Don't persist {userId, secret} for retry — the secret is a
|
|
5818
|
+
// bearer credential that any same-origin XSS could lift. On
|
|
5819
|
+
// rate limit the user must re-request the magic link from
|
|
5820
|
+
// their email (cleanupUrl has already stripped URL params).
|
|
5821
|
+
this.error = 'Rate limit exceeded. Please wait a moment and request a new magic link.';
|
|
5779
5822
|
this.isAuthenticated = false;
|
|
5780
5823
|
this.isAnonymous = false;
|
|
5781
5824
|
this.magicLinkExpired = false;
|
|
@@ -5901,15 +5944,10 @@ function handleMagicLinkCallbacks() {
|
|
|
5901
5944
|
|
|
5902
5945
|
const callbackInfo = event.detail;
|
|
5903
5946
|
|
|
5904
|
-
//
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
secret: callbackInfo.secret
|
|
5909
|
-
}));
|
|
5910
|
-
} catch (e) {
|
|
5911
|
-
console.warn('[Manifest Appwrite Auth] Could not store callback:', e);
|
|
5912
|
-
}
|
|
5947
|
+
// Note: we deliberately do NOT persist {userId, secret} to sessionStorage
|
|
5948
|
+
// as a retry safety net. The secret is a single-use bearer credential
|
|
5949
|
+
// that any same-origin XSS could read; UX of "refresh to retry on rate
|
|
5950
|
+
// limit" isn't worth the credential-exfil risk.
|
|
5913
5951
|
|
|
5914
5952
|
// Handle the callback
|
|
5915
5953
|
await store.handleMagicLinkCallback(callbackInfo.userId, callbackInfo.secret);
|
|
@@ -6247,16 +6285,11 @@ function initializeCallbacks() {
|
|
|
6247
6285
|
const secret = urlParams.get('secret');
|
|
6248
6286
|
const expire = urlParams.get('expire');
|
|
6249
6287
|
|
|
6250
|
-
//
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
storedCallback = JSON.parse(stored);
|
|
6256
|
-
}
|
|
6257
|
-
} catch (e) {
|
|
6258
|
-
// Ignore parse errors
|
|
6259
|
-
}
|
|
6288
|
+
// Eagerly clear any stale magic-link credentials left in sessionStorage
|
|
6289
|
+
// by an earlier (pre-fix) Manifest version. We no longer persist them,
|
|
6290
|
+
// and reading stale ones could resurrect a credential that should have
|
|
6291
|
+
// been forgotten.
|
|
6292
|
+
try { sessionStorage.removeItem('manifest:magic-link:callback'); } catch (e) {}
|
|
6260
6293
|
|
|
6261
6294
|
// Check OAuth redirect flag
|
|
6262
6295
|
const isOAuthCallback = sessionStorage.getItem('manifest:oauth:redirect') === 'true';
|
|
@@ -6267,14 +6300,14 @@ function initializeCallbacks() {
|
|
|
6267
6300
|
const isTeamInvite = !!(teamId && membershipId && userId && secret);
|
|
6268
6301
|
|
|
6269
6302
|
const callbackInfo = {
|
|
6270
|
-
userId: userId
|
|
6271
|
-
secret: secret
|
|
6303
|
+
userId: userId,
|
|
6304
|
+
secret: secret,
|
|
6272
6305
|
expire: expire,
|
|
6273
6306
|
teamId: teamId,
|
|
6274
6307
|
membershipId: membershipId,
|
|
6275
6308
|
isOAuth: isOAuthCallback,
|
|
6276
6309
|
isTeamInvite: isTeamInvite,
|
|
6277
|
-
hasCallback: !!
|
|
6310
|
+
hasCallback: !!userId && !!secret,
|
|
6278
6311
|
hasExpired: !!expire && !userId && !secret
|
|
6279
6312
|
};
|
|
6280
6313
|
|
package/lib/manifest.code.css
CHANGED
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
|
|
229
229
|
background-color: var(--color-page, oklch(100% 0 0));
|
|
230
230
|
border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
|
|
231
|
-
border-radius:
|
|
231
|
+
border-radius: calc(var(--radius, 0.5rem) * 2);
|
|
232
232
|
tab-size: 4;
|
|
233
233
|
white-space: pre;
|
|
234
234
|
white-space-collapse: preserve
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
line-height: 1.5;
|
|
262
262
|
background: color-mix(in oklch, var(--color-field-surface, oklch(91.79% 0.0029 264.26)) 45%, transparent);
|
|
263
263
|
border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
|
|
264
|
-
border-radius:
|
|
264
|
+
border-radius: calc(var(--radius, 0.5rem) * 2);
|
|
265
265
|
overflow: hidden;
|
|
266
266
|
|
|
267
267
|
&:has(> header) {
|
|
@@ -346,6 +346,8 @@
|
|
|
346
346
|
text-align: right;
|
|
347
347
|
color: var(--color-content-subtle, oklch(67.4% 0.0318 251.27));
|
|
348
348
|
background: var(--color-page, oklch(100% 0 0));
|
|
349
|
+
border: 1px solid var(--color-field-surface, oklch(91.79% 0.0029 264.26));
|
|
350
|
+
border-inline-end: 0 none;
|
|
349
351
|
pointer-events: none;
|
|
350
352
|
user-select: none;
|
|
351
353
|
}
|
|
@@ -364,13 +366,18 @@
|
|
|
364
366
|
border-radius: 0.875rem
|
|
365
367
|
}
|
|
366
368
|
|
|
369
|
+
&:not(> header) pre {
|
|
370
|
+
border: 0 none
|
|
371
|
+
}
|
|
372
|
+
|
|
367
373
|
&[numbers] pre {
|
|
374
|
+
border-inline-start: 0 none;
|
|
368
375
|
border-start-start-radius: 0;
|
|
369
376
|
border-end-start-radius: 0
|
|
370
377
|
}
|
|
371
378
|
|
|
372
379
|
/* Copy button */
|
|
373
|
-
& .copy {
|
|
380
|
+
& button.copy {
|
|
374
381
|
position: absolute;
|
|
375
382
|
top: 0.3rem;
|
|
376
383
|
right: 0.5rem;
|
|
@@ -378,13 +385,18 @@
|
|
|
378
385
|
min-width: 0;
|
|
379
386
|
height: 1.75rem;
|
|
380
387
|
padding: 0;
|
|
388
|
+
background: none;
|
|
389
|
+
|
|
390
|
+
&:hover {
|
|
391
|
+
background: var(--color-field-surface, silver)
|
|
392
|
+
}
|
|
381
393
|
|
|
382
394
|
&::after {
|
|
383
395
|
content: '';
|
|
384
396
|
display: block;
|
|
385
397
|
width: 0.8125rem;
|
|
386
398
|
height: 0.8125rem;
|
|
387
|
-
background-color: var(--color-
|
|
399
|
+
background-color: var(--color-content-neutral, gray);
|
|
388
400
|
mask-image: var(--icon-copy-code);
|
|
389
401
|
mask-size: 1rem;
|
|
390
402
|
mask-size: contain;
|
|
@@ -398,7 +410,7 @@
|
|
|
398
410
|
&.copied {
|
|
399
411
|
--color-field-surface: var(--color-positive-surface);
|
|
400
412
|
--color-field-surface-hover: var(--color-positive-surface-hover);
|
|
401
|
-
--color-
|
|
413
|
+
--color-content-neutral: var(--color-positive-inverse);
|
|
402
414
|
}
|
|
403
415
|
}
|
|
404
416
|
|
|
@@ -408,14 +420,14 @@
|
|
|
408
420
|
border: 0 none;
|
|
409
421
|
overflow: visible;
|
|
410
422
|
|
|
411
|
-
& .copy {
|
|
423
|
+
& button.copy {
|
|
412
424
|
top: -2.25rem
|
|
413
425
|
}
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
|
|
417
429
|
/* RTL support */
|
|
418
|
-
[dir=rtl] :where(x-code-group, x-code, [x-code]) .copy {
|
|
430
|
+
[dir=rtl] :where(x-code-group, x-code, [x-code]) button.copy {
|
|
419
431
|
right: auto;
|
|
420
432
|
left: 0.5rem
|
|
421
433
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@import url("https://fonts.googleapis.com/css2?family=Gabarito:wght@400..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Lexend+Exa&display=swap");:root{--icon-copy-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/g%3E%3C/svg%3E");--icon-copied-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--color-code-keyword:#b8860b;--color-code-string:#8b4513;--color-code-comment:
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Gabarito:wght@400..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Lexend+Exa&display=swap");:root{--icon-copy-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/g%3E%3C/svg%3E");--icon-copied-code:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--color-code-keyword:#b8860b;--color-code-string:#8b4513;--color-code-comment:gray;--color-code-function:peru;--color-code-number:sienna;--color-code-operator:#2f4f4f;--color-code-class-name:#daa520;--color-code-tag:#4682b4;--color-code-attr-name:#ff8c00;--color-code-attr-value:#8b4513;--color-code-property:sienna;--color-code-selector:#4682b4;--color-code-punctuation:#2f4f4f;--color-code-builtin:#b8860b;--color-code-constant:sienna;--color-code-boolean:sienna;--color-code-regex:#8b4513;--color-code-symbol:#daa520;--color-code-entity:#daa520;--color-code-url:sienna;--color-code-atrule:#b8860b;--color-code-rule:#4682b4;--color-code-doctype:gray;--color-code-cdata:gray;--color-code-prolog:gray;--color-code-namespace:gray;--color-code-important:#b8860b;--color-code-inserted:#228b22;--color-code-deleted:#dc143c;--color-code-char:#8b4513}.dark{--color-code-keyword:#f4a460;--color-code-string:#deb887;--color-code-comment:#8b8b8b;--color-code-function:#daa520;--color-code-number:tan;--color-code-operator:wheat;--color-code-class-name:peru;--color-code-tag:#87ceeb;--color-code-attr-name:gold;--color-code-attr-value:#deb887;--color-code-property:tan;--color-code-selector:#87ceeb;--color-code-punctuation:wheat;--color-code-builtin:#f4a460;--color-code-constant:tan;--color-code-boolean:tan;--color-code-regex:#deb887;--color-code-symbol:peru;--color-code-entity:peru;--color-code-url:tan;--color-code-atrule:#f4a460;--color-code-rule:#98fb98;--color-code-doctype:#8b8b8b;--color-code-cdata:#8b8b8b;--color-code-prolog:#8b8b8b;--color-code-namespace:#8b8b8b;--color-code-important:#f4a460;--color-code-inserted:#98fb98;--color-code-deleted:#f08080;--color-code-char:#deb887}@layer utilities{.hljs-comment{color:var(--color-code-comment)!important}.hljs-keyword{color:var(--color-code-keyword)!important}.hljs-string{color:var(--color-code-string)!important}.hljs-number{color:var(--color-code-number)!important}.hljs-literal{color:var(--color-code-constant)!important}.hljs-type{color:var(--color-code-class-name)!important}.hljs-variable{color:var(--color-code-property)!important}.hljs-variable.language_{color:var(--color-code-keyword)!important}.hljs-variable.constant_{color:var(--color-code-constant)!important}.hljs-title{color:var(--color-code-function)!important}.hljs-title.class_.inherited__{color:var(--color-code-class-name)!important}.hljs-title.function_.invoke__{color:var(--color-code-function)!important}.hljs-params{color:var(--color-code-property)!important}.hljs-doctag{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-meta{color:var(--color-code-comment)!important}.hljs-meta.keyword_,.hljs-meta.prompt_{color:var(--color-code-keyword)!important}.hljs-meta.string_{color:var(--color-code-string)!important}.hljs-section{color:var(--color-code-keyword)!important;font-weight:600!important}.hljs-name{color:var(--color-code-tag)!important}.hljs-attribute{color:var(--color-code-attr-name)!important}.hljs-bullet{color:var(--color-code-punctuation)!important}.hljs-code{color:var(--color-code-property)!important}.hljs-formula{color:var(--color-code-number)!important}.hljs-quote{color:var(--color-code-string)!important}.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo{color:var(--color-code-selector)!important}.hljs-template-tag{color:var(--color-code-tag)!important}.hljs-template-variable{color:var(--color-code-property)!important}.hljs-subst{color:var(--color-code-string)!important}}@layer components{:where(pre):not(.unstyle){display:flex;flex:1;-ms-overflow-style:scrollbar;background-color:var(--color-page,oklch(100% 0 0));border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);color:var(--color-content-neutral,oklch(48.26% .0365 255.09));font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow-x:auto;padding:calc(var(--spacing, .25rem)*4);tab-size:4;text-indent:0;white-space:pre;white-space-collapse:preserve;width:auto}:where(pre code):not(.unstyle){background-color:transparent;border:0;color:inherit;display:block;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0;width:100%;& span{vertical-align:initial}}:where(x-code-group,x-code,[x-code]){background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 45%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);display:flex;flex-flow:row wrap;font-family:IBM Plex Mono,monospace;font-size:.8125rem;line-height:1.5;overflow:hidden;position:relative;&:has(>header){padding:.125rem}&>header{align-items:center;border-bottom:none;border-radius:.8125rem .8125rem 0 0;color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-size:.8125rem;font-weight:500;gap:calc(var(--spacing, .25rem)*2);overflow-x:auto;padding:.5rem 4rem .5rem 1rem;width:100%;-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}& button[role=tab]{background:transparent;border-radius:var(--radius,.5rem);color:inherit;cursor:pointer;flex-shrink:0;font-family:inherit;font-size:inherit;height:fit-content;padding:var(--spacing,.25rem) calc(var(--spacing, .25rem)*2);transition:var(--transition,all .05s ease-in-out);&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26));color:var(--color-content-neutral,oklch(48.26% .0365 255.09))}&.selected{background:transparent;color:var(--color-brand-content,#de6618);position:relative;&:hover{background-color:var(--color-surface-3,oklch(91.79% .0029 264.26))}&:after{background:color-mix(in oklch,var(--color-brand-content,#de6618) 50%,transparent);border-radius:.5rem;bottom:-.5rem;content:"";height:2px;left:50%;position:absolute;transform:translateX(-50%);width:calc(100% - var(--spacing, .25rem)*4)}}}}& .lines{background:var(--color-page,oklch(100% 0 0));border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-inline-end:0 none;color:var(--color-content-subtle,oklch(67.4% .0318 251.27));display:flex;flex-direction:column;font-family:inherit;font-size:inherit;line-height:inherit;min-width:2.5rem;padding:calc(var(--spacing, .25rem)*4) .5rem calc(var(--spacing, .25rem)*4) .5rem;pointer-events:none;text-align:right;user-select:none;width:fit-content}&:has(>header) .lines{border-end-start-radius:.875rem;border-start-start-radius:.875rem}& pre{border:0;margin-top:0}&:has(>header) pre{border-radius:.875rem}&:not(>header) pre{border:0}&[numbers] pre{border-end-start-radius:0;border-inline-start:0 none;border-start-start-radius:0}& button.copy{background:none;height:1.75rem;min-width:0;padding:0;position:absolute;right:.5rem;top:.3rem;width:1.75rem;&:hover{background:var(--color-field-surface,silver)}&:after{background-color:var(--color-content-neutral,gray);content:"";display:block;height:.8125rem;mask-image:var(--icon-copy-code);mask-repeat:no-repeat;mask-size:1rem;mask-size:contain;width:.8125rem}&.copied:after{mask-image:var(--icon-copied-code)}&.copied{--color-field-surface:var(--color-positive-surface);--color-field-surface-hover:var(--color-positive-surface-hover);--color-content-neutral:var(--color-positive-inverse)}}:where(x-code){border:0;margin-top:0;overflow:visible;width:100%;& button.copy{top:-2.25rem}}}[dir=rtl] :where(x-code-group,x-code,[x-code]) button.copy{left:.5rem;right:auto}}@layer utilities{.prose aside.frame{background:color-mix(in oklch,var(--color-field-surface,oklch(91.79% .0029 264.26)) 35%,transparent);border:1px solid var(--color-field-surface,oklch(91.79% .0029 264.26));border-radius:calc(var(--radius, .5rem)*2);display:flex;gap:calc(var(--spacing, .25rem)*4)}.prose aside.frame:has(+x-code-group,+x-code){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.prose aside.frame+x-code,.prose aside.frame+x-code-group{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}.prose aside.frame+x-code pre{border-top-left-radius:0;border-top-right-radius:0}}
|
|
@@ -8,7 +8,7 @@ window.getManifestBase = function getManifestBase() {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
// Absolute pathname prefix for the app root (e.g. "/src/dist"). Used by router for links and route matching.
|
|
11
|
-
// Prerender injects <meta name="manifest:router-base" content="/path"> from manifest.
|
|
11
|
+
// Prerender injects <meta name="manifest:router-base" content="/path"> from manifest.render.routerBase or root+output. If present, use it; else fall back to depth or manifest link.
|
|
12
12
|
window.getManifestBasePath = function getManifestBasePath() {
|
|
13
13
|
const baseMeta = document.querySelector('meta[name="manifest:router-base"]');
|
|
14
14
|
const content = baseMeta?.getAttribute('content');
|
|
@@ -149,6 +149,28 @@ window.ManifestComponentsLoader = {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
// Components processor
|
|
152
|
+
|
|
153
|
+
// Escape a string so it's safe to interpolate inside a single-quoted JS
|
|
154
|
+
// string AND inside backtick template literals. The original escape covered
|
|
155
|
+
// the single-quote + whitespace cases but missed three vectors that matter
|
|
156
|
+
// when component templates use backtick literals (e.g. x-text="`Hi $modify('n')`"):
|
|
157
|
+
// - `\` → a trailing backslash would escape the closing quote
|
|
158
|
+
// - `` ` `` → terminates a backtick template literal
|
|
159
|
+
// - `${` → opens an interpolation that Alpine evaluates as JS
|
|
160
|
+
// Without escaping those, a bound value like `${alert(1)}` from a data
|
|
161
|
+
// source becomes code execution inside the wrapping template literal.
|
|
162
|
+
// Backslash must be escaped FIRST so the other replacements don't compound.
|
|
163
|
+
function escapeForSingleQuotedJsString(s) {
|
|
164
|
+
return String(s)
|
|
165
|
+
.replace(/\\/g, '\\\\')
|
|
166
|
+
.replace(/'/g, "\\'")
|
|
167
|
+
.replace(/`/g, '\\`')
|
|
168
|
+
.replace(/\$\{/g, '\\${')
|
|
169
|
+
.replace(/\r/g, '\\r')
|
|
170
|
+
.replace(/\n/g, '\\n')
|
|
171
|
+
.replace(/\t/g, '\\t');
|
|
172
|
+
}
|
|
173
|
+
|
|
152
174
|
window.ManifestComponentsProcessor = {
|
|
153
175
|
async processComponent(element, instanceId) {
|
|
154
176
|
const name = element.tagName.toLowerCase().replace('x-', '');
|
|
@@ -295,7 +317,7 @@ window.ManifestComponentsProcessor = {
|
|
|
295
317
|
return val;
|
|
296
318
|
}
|
|
297
319
|
// Always quote string values to ensure they're treated as strings, not variables
|
|
298
|
-
return `'${val
|
|
320
|
+
return `'${escapeForSingleQuotedJsString(val)}'`;
|
|
299
321
|
}
|
|
300
322
|
);
|
|
301
323
|
el.setAttribute(attr.name, newValue);
|
|
@@ -310,7 +332,7 @@ window.ManifestComponentsProcessor = {
|
|
|
310
332
|
el.setAttribute(attr.name, propValue);
|
|
311
333
|
} else {
|
|
312
334
|
// Always quote string values and escape special characters
|
|
313
|
-
const quotedValue = `'${propValue
|
|
335
|
+
const quotedValue = `'${escapeForSingleQuotedJsString(propValue)}'`;
|
|
314
336
|
el.setAttribute(attr.name, quotedValue);
|
|
315
337
|
}
|
|
316
338
|
}
|
|
@@ -776,158 +798,6 @@ window.ManifestComponentsMutation = {
|
|
|
776
798
|
}
|
|
777
799
|
};
|
|
778
800
|
|
|
779
|
-
// Components — route-level prefetch.
|
|
780
|
-
//
|
|
781
|
-
// Two enhancements that run on top of the existing on-encounter loader:
|
|
782
|
-
//
|
|
783
|
-
// 1. Parallel batch on route change. When manifest:route-change fires,
|
|
784
|
-
// scan the [x-route] subtrees that match the new route and call
|
|
785
|
-
// loadComponent() on every <x-*> tag inside them. The loader
|
|
786
|
-
// deduplicates fetches, so calling it for components that the
|
|
787
|
-
// regular swapping logic is already mounting is harmless — but
|
|
788
|
-
// pre-issuing in parallel saves 50–200 ms vs. one-by-one fetches.
|
|
789
|
-
//
|
|
790
|
-
// 2. Prefetch on hover. When the pointer enters an internal <a href>,
|
|
791
|
-
// derive the target pathname, find the [x-route] subtree(s) that
|
|
792
|
-
// would match it, and prefetch their components. By the time the
|
|
793
|
-
// user clicks the link, the components are warm in the loader's
|
|
794
|
-
// cache and navigation feels instant.
|
|
795
|
-
//
|
|
796
|
-
// Both phases require zero author configuration. Manifest auto-discovers
|
|
797
|
-
// what to prefetch from the existing [x-route] DOM structure.
|
|
798
|
-
|
|
799
|
-
(function () {
|
|
800
|
-
'use strict';
|
|
801
|
-
|
|
802
|
-
// <x-*> tag pattern — lowercase, hyphenated.
|
|
803
|
-
const TAG_RE = /^x-[a-z][a-z0-9-]*$/;
|
|
804
|
-
|
|
805
|
-
// Framework-provided web components (registered by Manifest plugins
|
|
806
|
-
// themselves, not as project components in manifest.json). Skip these
|
|
807
|
-
// when scanning for project components to prefetch.
|
|
808
|
-
const FRAMEWORK_TAGS = new Set(['code', 'code-group']);
|
|
809
|
-
|
|
810
|
-
// Anchors we've already issued a hover-prefetch for. WeakSet so DOM
|
|
811
|
-
// garbage-collects naturally as elements leave the tree.
|
|
812
|
-
const prefetchedAnchors = new WeakSet();
|
|
813
|
-
|
|
814
|
-
function loader() { return window.ManifestComponentsLoader; }
|
|
815
|
-
|
|
816
|
-
// Match a single route pattern against a normalized pathname (no
|
|
817
|
-
// leading/trailing slashes, '/' represented as '/'). Mirrors the
|
|
818
|
-
// router visibility logic so prefetch targets the same subtrees.
|
|
819
|
-
function routeMatches(routeValue, pathname) {
|
|
820
|
-
const pieces = String(routeValue || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
821
|
-
let matched = false;
|
|
822
|
-
let negated = false;
|
|
823
|
-
for (const piece of pieces) {
|
|
824
|
-
if (piece === '!*') continue; // catch-all only handled by visibility plugin
|
|
825
|
-
if (piece.startsWith('!')) {
|
|
826
|
-
if (piece.slice(1) === pathname) negated = true;
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
if (piece.startsWith('=')) {
|
|
830
|
-
if (piece.slice(1) === pathname) matched = true;
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
if (piece.endsWith('/*')) {
|
|
834
|
-
const prefix = piece.slice(0, -2);
|
|
835
|
-
if (pathname === prefix || pathname.startsWith(prefix + '/')) matched = true;
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (piece === pathname) { matched = true; continue; }
|
|
839
|
-
if (pathname.startsWith(piece + '/')) matched = true;
|
|
840
|
-
}
|
|
841
|
-
return matched && !negated;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function findRouteSubtrees(pathname) {
|
|
845
|
-
const normalized = (pathname || '/') === '/' ? '/' : pathname.replace(/^\/|\/$/g, '');
|
|
846
|
-
const out = [];
|
|
847
|
-
document.querySelectorAll('[x-route]').forEach((el) => {
|
|
848
|
-
const value = el.getAttribute('x-route') || '';
|
|
849
|
-
if (routeMatches(value, normalized)) out.push(el);
|
|
850
|
-
});
|
|
851
|
-
return out;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function discoverComponentNames(root) {
|
|
855
|
-
const names = new Set();
|
|
856
|
-
if (!root || !root.querySelectorAll) return names;
|
|
857
|
-
// querySelectorAll('*') is the fastest path for "every descendant".
|
|
858
|
-
// We filter by tag name in JS — there's no CSS selector for "tag
|
|
859
|
-
// name starts with x-". A page typically has a few thousand nodes,
|
|
860
|
-
// which scans in well under a millisecond.
|
|
861
|
-
root.querySelectorAll('*').forEach((el) => {
|
|
862
|
-
const tag = el.tagName.toLowerCase();
|
|
863
|
-
if (!tag.startsWith('x-') || !TAG_RE.test(tag)) return;
|
|
864
|
-
const name = tag.slice(2);
|
|
865
|
-
if (!FRAMEWORK_TAGS.has(name)) names.add(name);
|
|
866
|
-
});
|
|
867
|
-
return names;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function prefetchForRoute(pathname) {
|
|
871
|
-
const L = loader();
|
|
872
|
-
if (!L || typeof L.loadComponent !== 'function') return;
|
|
873
|
-
const subtrees = findRouteSubtrees(pathname);
|
|
874
|
-
if (!subtrees.length) return;
|
|
875
|
-
const names = new Set();
|
|
876
|
-
for (const subtree of subtrees) {
|
|
877
|
-
discoverComponentNames(subtree).forEach((n) => names.add(n));
|
|
878
|
-
}
|
|
879
|
-
names.forEach((name) => {
|
|
880
|
-
try { L.loadComponent(name); } catch { /* swallow — dedup is internal */ }
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function hrefToPathname(href) {
|
|
885
|
-
if (!href) return null;
|
|
886
|
-
if (/^(#|mailto:|tel:|javascript:)/i.test(href)) return null;
|
|
887
|
-
try {
|
|
888
|
-
const url = new URL(href, window.location.href);
|
|
889
|
-
if (url.origin !== window.location.origin) return null;
|
|
890
|
-
return url.pathname || '/';
|
|
891
|
-
} catch {
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function initialize() {
|
|
897
|
-
// 1) Parallel batch on route change.
|
|
898
|
-
window.addEventListener('manifest:route-change', (event) => {
|
|
899
|
-
const detail = (event && event.detail) || {};
|
|
900
|
-
const path = detail.normalizedPath || detail.to || '/';
|
|
901
|
-
const pathname = String(path).startsWith('/') ? String(path) : '/' + String(path);
|
|
902
|
-
prefetchForRoute(pathname);
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// 2) Hover prefetch. Use pointerover (bubbles) and check the closest
|
|
906
|
-
// anchor on each event so we get a single trigger per anchor entry
|
|
907
|
-
// without needing pointerenter (which doesn't bubble). Dedup via
|
|
908
|
-
// a WeakSet so repeat moves within the anchor don't re-scan.
|
|
909
|
-
document.addEventListener('pointerover', (e) => {
|
|
910
|
-
if (!e.target || !e.target.closest) return;
|
|
911
|
-
const a = e.target.closest('a[href]');
|
|
912
|
-
if (!a || prefetchedAnchors.has(a)) return;
|
|
913
|
-
// Author opt-out: `data-no-prefetch` skips this anchor.
|
|
914
|
-
if (a.hasAttribute('data-no-prefetch')) return;
|
|
915
|
-
const href = a.getAttribute('href');
|
|
916
|
-
const pathname = hrefToPathname(href);
|
|
917
|
-
if (!pathname) return;
|
|
918
|
-
prefetchedAnchors.add(a);
|
|
919
|
-
prefetchForRoute(pathname);
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (document.readyState === 'loading') {
|
|
924
|
-
document.addEventListener('DOMContentLoaded', initialize);
|
|
925
|
-
} else {
|
|
926
|
-
initialize();
|
|
927
|
-
}
|
|
928
|
-
})();
|
|
929
|
-
|
|
930
|
-
|
|
931
801
|
// Main initialization for Manifest Components
|
|
932
802
|
function initializeComponents() {
|
|
933
803
|
if (window.ManifestComponentsRegistry) window.ManifestComponentsRegistry.initialize();
|