mnfst 0.5.199-next.2 → 0.5.199-next.4
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 +6 -5
- package/lib/manifest.appwrite.data.js +1 -1
- package/lib/manifest.chat.js +36 -4
- package/lib/manifest.components.js +2 -0
- package/lib/manifest.d.ts +32 -22
- package/lib/manifest.data.js +11 -6
- package/lib/manifest.defer.js +1 -1
- package/lib/manifest.device.js +491 -0
- package/lib/manifest.edit.js +32 -4
- package/lib/manifest.integrity.json +14 -13
- package/lib/manifest.js +34 -16
- package/lib/manifest.native.js +1 -1
- package/lib/manifest.payments.js +3 -2
- package/lib/manifest.schema.json +6 -1
- package/lib/manifest.status.js +5 -4
- package/lib/manifest.sw.js +1 -1
- package/lib/manifest.text.edit.js +46 -5
- package/package.json +1 -1
|
@@ -26,13 +26,14 @@ async function ensureManifest() {
|
|
|
26
26
|
return window.__manifestLoaded;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
if (window.__manifestPromise) {
|
|
30
|
+
const shared = await window.__manifestPromise.catch(() => null);
|
|
31
|
+
if (shared) return shared;
|
|
32
|
+
}
|
|
29
33
|
try {
|
|
30
34
|
const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// No-loader path: resolve ${VAR} placeholders the dynamic loader would have.
|
|
34
|
-
window.ManifestDataConfig?.interpolateManifest?.(manifest);
|
|
35
|
-
return manifest;
|
|
35
|
+
window.__manifestPromise = fetch(manifestUrl).then(r => r.json()).then(m => { window.ManifestDataConfig?.interpolateManifest?.(m); return m; });
|
|
36
|
+
return await window.__manifestPromise;
|
|
36
37
|
} catch (error) {
|
|
37
38
|
return null;
|
|
38
39
|
}
|
package/lib/manifest.chat.js
CHANGED
|
@@ -121,10 +121,20 @@
|
|
|
121
121
|
state.resetQueued = true;
|
|
122
122
|
queueMicrotask(() => { state.resetQueued = false; reset(); });
|
|
123
123
|
};
|
|
124
|
-
window
|
|
124
|
+
// pending → resolved at boot is not a switch: keep every window, hydrate the ones still loading, write under the real scope
|
|
125
|
+
window.addEventListener('manifest:persist:scope', (e) => { if (e.detail && e.detail.boot) adoptScope(); else queue(); });
|
|
125
126
|
for (const type of WIPE_EVENTS) window.addEventListener(type, queue);
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
function adoptScope() {
|
|
130
|
+
state.recent = null; state.indexPromise = null; state.indexDirty = false;
|
|
131
|
+
state.saved.clear(); state.windows.clear();
|
|
132
|
+
for (const [id, h] of state.handles) {
|
|
133
|
+
hydrate(id).then(w => { if (w && h.hydrate) h.hydrate(w.messages); }).catch(noop);
|
|
134
|
+
touch(id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
// Drop every in-memory window and the index; the store's scope prefix already
|
|
129
139
|
// covers the records. Handles go idle — the app re-opens for the new scope.
|
|
130
140
|
function reset() {
|
|
@@ -143,9 +153,26 @@
|
|
|
143
153
|
const r = records();
|
|
144
154
|
const ok = rec && typeof rec === 'object' && rec.scope === (r ? r.scope() : '') && Array.isArray(rec.recent);
|
|
145
155
|
state.recent = ok ? rec.recent.filter(id => typeof id === 'string' || typeof id === 'number') : [];
|
|
156
|
+
pruneIndex();
|
|
146
157
|
return state.recent;
|
|
147
158
|
}
|
|
148
159
|
|
|
160
|
+
// Drop index ids that have no record (entries from before slots were earned on first write)
|
|
161
|
+
function pruneIndex() {
|
|
162
|
+
const r = records(); const ids = state.recent;
|
|
163
|
+
if (!r || !ids || !ids.length) return;
|
|
164
|
+
const gen = state.generation;
|
|
165
|
+
const asked = ids.slice(); // the live array may be reordered by a promote while the read is in flight
|
|
166
|
+
r.get(asked.map(id => r.key('chat', id))).then(got => {
|
|
167
|
+
if (gen !== state.generation || state.recent !== ids || !got) return;
|
|
168
|
+
const found = new Set(asked.filter((id, i) => !!got[i]));
|
|
169
|
+
const keep = ids.filter(id => found.has(id) || (state.handles.get(id) && state.handles.get(id).count() > 0));
|
|
170
|
+
if (keep.length === ids.length) return;
|
|
171
|
+
ids.length = 0; ids.push(...keep);
|
|
172
|
+
state.indexDirty = true; state.pending.set(INDEX, Date.now() + WRITE_DEBOUNCE_MS); schedule();
|
|
173
|
+
}).catch(noop);
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
function ensureIndex() {
|
|
150
177
|
if (state.recent) return Promise.resolve(state.recent);
|
|
151
178
|
if (state.indexPromise) return state.indexPromise;
|
|
@@ -451,10 +478,15 @@
|
|
|
451
478
|
function commitParticipants() { state.participants = [..._participants.values()]; rev().n++; }
|
|
452
479
|
function commitTyping() { state.typing = [..._typing.values()]; rev().n++; }
|
|
453
480
|
|
|
454
|
-
// server echo reconciles body/media/status onto the local copy
|
|
481
|
+
// server echo reconciles body/media/status onto the local copy; meta merges so a
|
|
482
|
+
// re-delivery from another source (no translation, no channel) never drops fields
|
|
455
483
|
function merge(target, incoming) {
|
|
456
484
|
Object.assign(target.body, incoming.body);
|
|
457
|
-
for (const k of Object.keys(incoming))
|
|
485
|
+
for (const k of Object.keys(incoming)) {
|
|
486
|
+
if (k === 'body' || k === '_seq') continue;
|
|
487
|
+
if (k === 'meta' && target.meta && incoming.meta && typeof incoming.meta === 'object') { Object.assign(target.meta, incoming.meta); continue; }
|
|
488
|
+
target[k] = incoming[k];
|
|
489
|
+
}
|
|
458
490
|
}
|
|
459
491
|
|
|
460
492
|
// own echo beating the ack: claim the pending row by meta.clientId
|
|
@@ -584,7 +616,7 @@
|
|
|
584
616
|
commit(); commitParticipants(); commitTyping();
|
|
585
617
|
}
|
|
586
618
|
|
|
587
|
-
if (P() && !isAggregate && opts.
|
|
619
|
+
if (P() && !isAggregate && opts.persistWindow !== false) detach = P().attach(conversationId, { snapshot, reset, hydrate: hydrateWindow, stale: () => state.stale, count: () => _msgs.length });
|
|
588
620
|
|
|
589
621
|
async function open() {
|
|
590
622
|
state.status = 'loading';
|
|
@@ -61,6 +61,7 @@ window.ManifestComponentsRegistry = {
|
|
|
61
61
|
// manifest on window yet). This must be async — a synchronous XHR on
|
|
62
62
|
// the main thread is deprecated and was flagged by PageSpeed.
|
|
63
63
|
let manifest = window.__manifestLoaded || this.manifest;
|
|
64
|
+
if (!manifest && window.__manifestPromise) manifest = await window.__manifestPromise.catch(() => null);
|
|
64
65
|
if (!manifest) {
|
|
65
66
|
try {
|
|
66
67
|
const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
|
|
@@ -72,6 +73,7 @@ window.ManifestComponentsRegistry = {
|
|
|
72
73
|
manifest = await res.json();
|
|
73
74
|
// No-loader path: resolve ${VAR} placeholders the dynamic loader would have.
|
|
74
75
|
window.ManifestDataConfig?.interpolateManifest?.(manifest);
|
|
76
|
+
window.__manifestPromise = Promise.resolve(manifest); // share with plugins that init after us
|
|
75
77
|
} else {
|
|
76
78
|
console.warn('[Manifest] Failed to load manifest.json (HTTP', res.status + ')');
|
|
77
79
|
}
|
package/lib/manifest.d.ts
CHANGED
|
@@ -268,7 +268,7 @@ export type ManifestExport = (opts?: {
|
|
|
268
268
|
[extra: string]: unknown;
|
|
269
269
|
}) => Promise<unknown>;
|
|
270
270
|
|
|
271
|
-
/** `$device` from the utilities plugin (enriched by the
|
|
271
|
+
/** `$device` from the utilities plugin (enriched by the device plugin when present). */
|
|
272
272
|
export interface ManifestDevice {
|
|
273
273
|
readonly os: string;
|
|
274
274
|
readonly touch: boolean;
|
|
@@ -278,18 +278,25 @@ export interface ManifestDevice {
|
|
|
278
278
|
readonly platform: string;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
/** `$share` from the
|
|
281
|
+
/** `$share` from the device plugin — native sheet, web share, or clipboard fallback. */
|
|
282
282
|
export type ManifestShare = (opts?: { title?: string; text?: string; url?: string }) =>
|
|
283
283
|
Promise<{ shared: boolean; method: string; cancelled?: boolean }>;
|
|
284
284
|
|
|
285
|
-
/** `$push` from the
|
|
285
|
+
/** `$push` from the device plugin — push/notification permissions and token. */
|
|
286
286
|
export interface ManifestPush {
|
|
287
|
-
|
|
287
|
+
/** Ask for permission, on your schedule (never prompted on load). */
|
|
288
|
+
request(): Promise<'granted' | 'denied' | 'prompt' | 'unsupported' | string>;
|
|
289
|
+
/** Register with the platform push service (no-op on the web). */
|
|
290
|
+
register(): Promise<unknown>;
|
|
291
|
+
onToken(handler: (token: string) => void): void;
|
|
292
|
+
onReceive(handler: (notification: unknown) => void): void;
|
|
293
|
+
onTap(handler: (notification: unknown) => void): void;
|
|
288
294
|
readonly permission: string;
|
|
295
|
+
readonly token: string | null;
|
|
289
296
|
[extra: string]: unknown;
|
|
290
297
|
}
|
|
291
298
|
|
|
292
|
-
/** `$secure` from the
|
|
299
|
+
/** `$secure` from the device plugin — secure key-value storage (native keychain or fallback). */
|
|
293
300
|
export interface ManifestSecure {
|
|
294
301
|
get(key: string): Promise<string | null>;
|
|
295
302
|
set(key: string, value: string): Promise<void>;
|
|
@@ -299,33 +306,36 @@ export interface ManifestSecure {
|
|
|
299
306
|
use(adapter: unknown): void;
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
/** `$links` from the
|
|
309
|
+
/** `$links` from the device plugin — deep/universal link handling. */
|
|
303
310
|
export interface ManifestLinks {
|
|
304
|
-
|
|
311
|
+
/** Take over an inbound link: link.url is the full URL, link.path the route it maps to. */
|
|
312
|
+
on(handler: (link: { url: string; path: string }) => void): void;
|
|
305
313
|
open(url: string): void;
|
|
306
314
|
readonly last: string | null;
|
|
307
315
|
}
|
|
308
316
|
|
|
309
|
-
/** `$app` from the
|
|
317
|
+
/** `$app` from the device plugin — foreground/background state. */
|
|
310
318
|
export interface ManifestApp {
|
|
311
319
|
readonly active: boolean;
|
|
312
320
|
onChange(handler: (active: boolean) => void): void;
|
|
313
321
|
}
|
|
314
322
|
|
|
315
|
-
/** `$haptics` from the
|
|
323
|
+
/** `$haptics` from the device plugin — haptic feedback with web vibration fallback. */
|
|
316
324
|
export interface ManifestHaptics {
|
|
317
|
-
impact(style?: string): Promise<unknown>;
|
|
318
|
-
notification(type?: string): Promise<unknown>;
|
|
325
|
+
impact(style?: 'LIGHT' | 'MEDIUM' | 'HEAVY' | string): Promise<unknown>;
|
|
326
|
+
notification(type?: 'SUCCESS' | 'WARNING' | 'ERROR' | string): Promise<unknown>;
|
|
327
|
+
selection(): Promise<unknown>;
|
|
328
|
+
vibrate(ms?: number): Promise<unknown> | unknown;
|
|
319
329
|
[extra: string]: unknown;
|
|
320
330
|
}
|
|
321
331
|
|
|
322
|
-
/** `$biometric` from the
|
|
332
|
+
/** `$biometric` from the device plugin. */
|
|
323
333
|
export interface ManifestBiometric {
|
|
324
334
|
available(): Promise<boolean>;
|
|
325
335
|
verify(opts?: Record<string, unknown>): Promise<unknown>;
|
|
326
336
|
}
|
|
327
337
|
|
|
328
|
-
/** `$camera` from the
|
|
338
|
+
/** `$camera` from the device plugin. */
|
|
329
339
|
export interface ManifestCamera {
|
|
330
340
|
photo(opts?: Record<string, unknown>): Promise<unknown>;
|
|
331
341
|
pick(opts?: Record<string, unknown>): Promise<unknown>;
|
|
@@ -465,23 +475,23 @@ declare global {
|
|
|
465
475
|
const $edit: ManifestEdit;
|
|
466
476
|
/** Text editor (text-edit plugin). */
|
|
467
477
|
const $text: ManifestTextEdit;
|
|
468
|
-
/** Device/platform info (utilities; enriched by the
|
|
478
|
+
/** Device/platform info (utilities; enriched by the device plugin). */
|
|
469
479
|
const $device: ManifestDevice;
|
|
470
|
-
/** Native share (
|
|
480
|
+
/** Native share (device plugin). */
|
|
471
481
|
const $share: ManifestShare;
|
|
472
|
-
/** Push permissions (
|
|
482
|
+
/** Push permissions (device plugin). */
|
|
473
483
|
const $push: ManifestPush;
|
|
474
|
-
/** Secure storage (
|
|
484
|
+
/** Secure storage (device plugin). */
|
|
475
485
|
const $secure: ManifestSecure;
|
|
476
|
-
/** Deep links (
|
|
486
|
+
/** Deep links (device plugin). */
|
|
477
487
|
const $links: ManifestLinks;
|
|
478
|
-
/** App foreground state (
|
|
488
|
+
/** App foreground state (device plugin). */
|
|
479
489
|
const $app: ManifestApp;
|
|
480
|
-
/** Haptics (
|
|
490
|
+
/** Haptics (device plugin). */
|
|
481
491
|
const $haptics: ManifestHaptics;
|
|
482
|
-
/** Biometric auth (
|
|
492
|
+
/** Biometric auth (device plugin). */
|
|
483
493
|
const $biometric: ManifestBiometric;
|
|
484
|
-
/** Camera (
|
|
494
|
+
/** Camera (device plugin). */
|
|
485
495
|
const $camera: ManifestCamera;
|
|
486
496
|
|
|
487
497
|
/** Window-level Manifest namespace exposed by the loader. */
|
package/lib/manifest.data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.199-next.
|
|
5
|
+
const MANIFEST_BUILD_VERSION = '0.5.199-next.4';
|
|
6
6
|
|
|
7
7
|
/* Manifest Data Sources - Configuration */
|
|
8
8
|
|
|
@@ -21,12 +21,15 @@ async function ensureManifest() {
|
|
|
21
21
|
return window.__manifestLoaded;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// One request per boot, shared with the loader and the other plugins (window.__manifestPromise)
|
|
25
|
+
if (window.__manifestPromise) {
|
|
26
|
+
const shared = await window.__manifestPromise.catch(() => null);
|
|
27
|
+
if (looksComplete(shared)) return shared;
|
|
28
|
+
}
|
|
24
29
|
try {
|
|
25
30
|
const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
interpolateManifest(manifest);
|
|
29
|
-
return manifest;
|
|
31
|
+
window.__manifestPromise = fetch(manifestUrl).then(r => r.json()).then(m => { interpolateManifest(m); return m; });
|
|
32
|
+
return await window.__manifestPromise;
|
|
30
33
|
} catch (error) {
|
|
31
34
|
console.error('[Manifest Data] Failed to load manifest:', error);
|
|
32
35
|
return null;
|
|
@@ -1623,6 +1626,7 @@ window.ManifestDataStore = {
|
|
|
1623
1626
|
// Reads `persist` per source and top-level `persistence`; true when anything opted in
|
|
1624
1627
|
function configure(manifest) {
|
|
1625
1628
|
state.configured = true;
|
|
1629
|
+
state.manifest = manifest || null;
|
|
1626
1630
|
state.sources.clear();
|
|
1627
1631
|
for (const [name, source] of Object.entries(manifest?.data || {})) {
|
|
1628
1632
|
if (!source || typeof source !== 'object') continue;
|
|
@@ -1652,6 +1656,7 @@ window.ManifestDataStore = {
|
|
|
1652
1656
|
if (state.enabled || state.disabled) return state.enabled;
|
|
1653
1657
|
if (typeof indexedDB === 'undefined' || !indexedDB) return false;
|
|
1654
1658
|
state.enabled = true;
|
|
1659
|
+
watchAuthSettle(state.manifest);
|
|
1655
1660
|
if (!state.dbName) state.dbName = `manifest:${(typeof location !== 'undefined' && location.origin) || 'null'}`;
|
|
1656
1661
|
if (!state.frameworkVersion) state.frameworkVersion = buildVersion;
|
|
1657
1662
|
state.scope = evaluateScope();
|
|
@@ -1794,7 +1799,7 @@ window.ManifestDataStore = {
|
|
|
1794
1799
|
if (wasPending) onLanded([...state.sources.keys()]); // rows that landed while pending get written under the real scope
|
|
1795
1800
|
}
|
|
1796
1801
|
if (typeof window !== 'undefined') {
|
|
1797
|
-
try { window.dispatchEvent(new CustomEvent('manifest:persist:scope', { detail: { scope: next, previous: prev } })); } catch { /* no-op */ }
|
|
1802
|
+
try { window.dispatchEvent(new CustomEvent('manifest:persist:scope', { detail: { scope: next, previous: prev, boot: wasPending } })); } catch { /* no-op */ }
|
|
1798
1803
|
}
|
|
1799
1804
|
}
|
|
1800
1805
|
|
package/lib/manifest.defer.js
CHANGED
|
@@ -32,7 +32,7 @@ const URGENT_WINDOW_MS = 1500;
|
|
|
32
32
|
const URGENT_CAP = 8;
|
|
33
33
|
const PREWARM_CAP = (window.ManifestDeferConfig && window.ManifestDeferConfig.prewarmCap) || 48; // warm-but-unopened containers kept live
|
|
34
34
|
const IDLE_BUDGET_MS = 4;
|
|
35
|
-
const SLICE_MAX =
|
|
35
|
+
const SLICE_MAX = 2; // containers per idle slice: one big menu render is already a long task, so keep slices short
|
|
36
36
|
const warm = new Set(); // prewarmed, never opened by the user
|
|
37
37
|
let lastGestureAt = -Infinity;
|
|
38
38
|
let urgentWindowAt = -Infinity;
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/* manifest.device.js — built from scripts/device/ */
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
|
|
5
|
+
// Manifest Native — umbrella core: Capacitor detection + $device enrichment.
|
|
6
|
+
// Capacitor injects window.Capacitor (+ window.Capacitor.Plugins.<Name>) in the
|
|
7
|
+
// native container. Every capability degrades to a web equivalent when absent,
|
|
8
|
+
// so this plugin is safe to load on the web build too.
|
|
9
|
+
|
|
10
|
+
function manifestNativeIsNative() {
|
|
11
|
+
const cap = window.Capacitor;
|
|
12
|
+
return !!(cap && (typeof cap.isNativePlatform !== 'function' || cap.isNativePlatform()));
|
|
13
|
+
}
|
|
14
|
+
function manifestNativePlugin(name) {
|
|
15
|
+
const cap = window.Capacitor;
|
|
16
|
+
return (cap && cap.Plugins && cap.Plugins[name]) || null;
|
|
17
|
+
}
|
|
18
|
+
function manifestNativePlatform() {
|
|
19
|
+
const cap = window.Capacitor;
|
|
20
|
+
if (cap && typeof cap.getPlatform === 'function') { try { return cap.getPlatform(); } catch (e) {} }
|
|
21
|
+
return manifestNativeIsNative() ? (document.documentElement.getAttribute('data-os') || 'native') : 'web';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Authoritatively stamp container/platform for $device + CSS variants.
|
|
25
|
+
function manifestNativeStamp() {
|
|
26
|
+
try {
|
|
27
|
+
if (!manifestNativeIsNative()) return;
|
|
28
|
+
const html = document.documentElement;
|
|
29
|
+
html.setAttribute('data-native', '');
|
|
30
|
+
const p = manifestNativePlatform();
|
|
31
|
+
if (p && p !== 'web') html.setAttribute('data-platform', p);
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
manifestNativeStamp();
|
|
35
|
+
|
|
36
|
+
// Register capability magics once Alpine is available.
|
|
37
|
+
let manifestNativeInitialized = false;
|
|
38
|
+
function initManifestNative() {
|
|
39
|
+
// Network runs here (not at load) so the Capacitor Network reading can reach
|
|
40
|
+
// the $device store, which utilities registers on alpine:init.
|
|
41
|
+
if (typeof initManifestNativeNetwork === 'function') initManifestNativeNetwork();
|
|
42
|
+
if (typeof initManifestShare === 'function') initManifestShare();
|
|
43
|
+
if (typeof initManifestSecure === 'function') initManifestSecure();
|
|
44
|
+
if (typeof initManifestLinks === 'function') initManifestLinks();
|
|
45
|
+
if (typeof initManifestPush === 'function') initManifestPush();
|
|
46
|
+
if (typeof initManifestApp === 'function') initManifestApp();
|
|
47
|
+
if (typeof initManifestHaptics === 'function') initManifestHaptics();
|
|
48
|
+
if (typeof initManifestBiometric === 'function') initManifestBiometric();
|
|
49
|
+
if (typeof initManifestCamera === 'function') initManifestCamera();
|
|
50
|
+
}
|
|
51
|
+
function ensureManifestNativeInitialized() {
|
|
52
|
+
if (manifestNativeInitialized) return;
|
|
53
|
+
if (!window.Alpine || typeof window.Alpine.magic !== 'function') return;
|
|
54
|
+
manifestNativeInitialized = true;
|
|
55
|
+
initManifestNative();
|
|
56
|
+
}
|
|
57
|
+
window.ensureManifestNativeInitialized = ensureManifestNativeInitialized;
|
|
58
|
+
document.addEventListener('alpine:init', ensureManifestNativeInitialized);
|
|
59
|
+
if (window.Alpine && typeof window.Alpine.magic === 'function') {
|
|
60
|
+
setTimeout(ensureManifestNativeInitialized, 0);
|
|
61
|
+
} else {
|
|
62
|
+
const manifestNativeCheck = setInterval(() => {
|
|
63
|
+
if (window.Alpine && typeof window.Alpine.magic === 'function') {
|
|
64
|
+
clearInterval(manifestNativeCheck);
|
|
65
|
+
ensureManifestNativeInitialized();
|
|
66
|
+
}
|
|
67
|
+
}, 10);
|
|
68
|
+
setTimeout(() => clearInterval(manifestNativeCheck), 5000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// $share — native share sheet (Capacitor Share) → Web Share API → clipboard.
|
|
73
|
+
// Resolves to { shared, method: 'native'|'web'|'clipboard'|'none', cancelled? }.
|
|
74
|
+
|
|
75
|
+
function manifestShareCancelled(e) {
|
|
76
|
+
return !!(e && (e.code === 'CANCELED' || e.name === 'AbortError' || /cancel|abort/i.test(e.message || '')));
|
|
77
|
+
}
|
|
78
|
+
function manifestShareClipboard(payload) {
|
|
79
|
+
const text = payload.url || payload.text || payload.title || '';
|
|
80
|
+
if (text && navigator.clipboard && navigator.clipboard.writeText) {
|
|
81
|
+
return navigator.clipboard.writeText(text)
|
|
82
|
+
.then(() => ({ shared: true, method: 'clipboard' }))
|
|
83
|
+
.catch(() => ({ shared: false, method: 'none' }));
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve({ shared: false, method: 'none' });
|
|
86
|
+
}
|
|
87
|
+
function manifestShareWeb(payload) {
|
|
88
|
+
if (navigator.share) {
|
|
89
|
+
return navigator.share({ title: payload.title, text: payload.text, url: payload.url })
|
|
90
|
+
.then(() => ({ shared: true, method: 'web' }))
|
|
91
|
+
.catch(e => manifestShareCancelled(e)
|
|
92
|
+
? { shared: false, method: 'web', cancelled: true }
|
|
93
|
+
: manifestShareClipboard(payload));
|
|
94
|
+
}
|
|
95
|
+
return manifestShareClipboard(payload);
|
|
96
|
+
}
|
|
97
|
+
function manifestShare(opts) {
|
|
98
|
+
const payload = opts || {};
|
|
99
|
+
const Share = manifestNativePlugin('Share');
|
|
100
|
+
if (Share) {
|
|
101
|
+
// When the native sheet is present, never fall through to the web sheet
|
|
102
|
+
// (that would double-prompt); report the native outcome instead.
|
|
103
|
+
return Share.share({ title: payload.title, text: payload.text, url: payload.url, dialogTitle: payload.dialogTitle })
|
|
104
|
+
.then(() => ({ shared: true, method: 'native' }))
|
|
105
|
+
.catch(e => manifestShareCancelled(e)
|
|
106
|
+
? { shared: false, method: 'native', cancelled: true }
|
|
107
|
+
: { shared: false, method: 'native', error: (e && (e.message || e.code)) || 'failed' });
|
|
108
|
+
}
|
|
109
|
+
return manifestShareWeb(payload);
|
|
110
|
+
}
|
|
111
|
+
function initManifestShare() {
|
|
112
|
+
window.Alpine.magic('share', () => manifestShare);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
// Network — higher-fidelity connectivity via Capacitor Network, feeding $device.
|
|
117
|
+
// On the web the base $device signal already tracks navigator online/offline;
|
|
118
|
+
// this only upgrades fidelity inside the native container.
|
|
119
|
+
|
|
120
|
+
function manifestNativeApplyOnline(connected) {
|
|
121
|
+
const online = connected !== false;
|
|
122
|
+
try {
|
|
123
|
+
document.documentElement.setAttribute('data-online', online ? 'true' : 'false');
|
|
124
|
+
if (window.Alpine && typeof window.Alpine.store === 'function') {
|
|
125
|
+
const store = window.Alpine.store('device');
|
|
126
|
+
if (store) store.online = online;
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {}
|
|
129
|
+
}
|
|
130
|
+
function initManifestNativeNetwork() {
|
|
131
|
+
const Network = manifestNativePlugin('Network');
|
|
132
|
+
if (!Network) return;
|
|
133
|
+
try { Network.getStatus().then(s => manifestNativeApplyOnline(s && s.connected)).catch(() => {}); } catch (e) {}
|
|
134
|
+
try { Network.addListener('networkStatusChange', s => manifestNativeApplyOnline(s && s.connected)); } catch (e) {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// $secure — Keychain/Keystore-backed key/value storage (native) with a
|
|
139
|
+
// namespaced localStorage fallback on the web. Intended for session tokens etc.
|
|
140
|
+
// Native: install a Capacitor secure-storage plugin registered as
|
|
141
|
+
// window.Capacitor.Plugins.SecureStorage; if its method shape differs from the
|
|
142
|
+
// adapters below, override with $secure.use(adapter). Verify on device.
|
|
143
|
+
// The web fallback is NOT encrypted — it degrades honestly, not securely.
|
|
144
|
+
|
|
145
|
+
const MANIFEST_SECURE_NS = 'mnfst:';
|
|
146
|
+
|
|
147
|
+
function manifestSecureWebBackend() {
|
|
148
|
+
const ls = () => window.localStorage;
|
|
149
|
+
return {
|
|
150
|
+
async get(key) { try { return ls().getItem(MANIFEST_SECURE_NS + key); } catch (e) { return null; } },
|
|
151
|
+
async set(key, value) { try { ls().setItem(MANIFEST_SECURE_NS + key, String(value)); } catch (e) {} },
|
|
152
|
+
async remove(key) { try { ls().removeItem(MANIFEST_SECURE_NS + key); } catch (e) {} },
|
|
153
|
+
async keys() {
|
|
154
|
+
try { return Object.keys(ls()).filter(k => k.indexOf(MANIFEST_SECURE_NS) === 0).map(k => k.slice(MANIFEST_SECURE_NS.length)); }
|
|
155
|
+
catch (e) { return []; }
|
|
156
|
+
},
|
|
157
|
+
async clear() { try { const ks = await this.keys(); ks.forEach(k => ls().removeItem(MANIFEST_SECURE_NS + k)); } catch (e) {} }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Adapts the two common Capacitor secure-storage plugin shapes: localStorage-like
|
|
162
|
+
// (getItem/setItem/removeItem) or object-arg (get/set/remove with {key,value}).
|
|
163
|
+
function manifestSecureNativeBackend(plugin) {
|
|
164
|
+
const itemApi = typeof plugin.getItem === 'function';
|
|
165
|
+
return {
|
|
166
|
+
async get(key) {
|
|
167
|
+
try {
|
|
168
|
+
if (itemApi) { const v = await plugin.getItem(key); return v == null ? null : v; }
|
|
169
|
+
const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
|
|
170
|
+
} catch (e) { return null; }
|
|
171
|
+
},
|
|
172
|
+
async set(key, value) {
|
|
173
|
+
try { if (itemApi) await plugin.setItem(key, String(value)); else await plugin.set({ key, value: String(value) }); } catch (e) {}
|
|
174
|
+
},
|
|
175
|
+
async remove(key) {
|
|
176
|
+
try { if (typeof plugin.removeItem === 'function') await plugin.removeItem(key); else await plugin.remove({ key }); } catch (e) {}
|
|
177
|
+
},
|
|
178
|
+
async keys() { try { const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || []; } catch (e) { return []; } },
|
|
179
|
+
async clear() { try { await plugin.clear(); } catch (e) {} }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let _manifestSecureBackend = null;
|
|
184
|
+
function manifestSecureBackend() {
|
|
185
|
+
if (_manifestSecureBackend) return _manifestSecureBackend;
|
|
186
|
+
const plugin = manifestNativePlugin('SecureStorage');
|
|
187
|
+
_manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin) : manifestSecureWebBackend();
|
|
188
|
+
return _manifestSecureBackend;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function initManifestSecure() {
|
|
192
|
+
window.Alpine.magic('secure', () => ({
|
|
193
|
+
get: (k) => manifestSecureBackend().get(k),
|
|
194
|
+
set: (k, v) => manifestSecureBackend().set(k, v),
|
|
195
|
+
remove: (k) => manifestSecureBackend().remove(k),
|
|
196
|
+
keys: () => manifestSecureBackend().keys(),
|
|
197
|
+
clear: () => manifestSecureBackend().clear(),
|
|
198
|
+
use: (adapter) => { _manifestSecureBackend = adapter; }
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// $links — deep / universal links. Native: Capacitor App fires appUrlOpen (and a
|
|
204
|
+
// cold-start launch URL); Manifest extracts the path and hands off to the router.
|
|
205
|
+
// $links.on(fn) lets the author take over routing (e.g. load data for /order/123).
|
|
206
|
+
// On the web there's no bridge, but $links.open(url) still works for programmatic
|
|
207
|
+
// deep-linking and testing. Couples with lifecycle + push (tap → route).
|
|
208
|
+
|
|
209
|
+
function manifestLinkPath(url) {
|
|
210
|
+
const s = String(url);
|
|
211
|
+
const scheme = (s.match(/^([a-z][a-z0-9+.-]*):/i) || [])[1];
|
|
212
|
+
if (scheme && scheme.toLowerCase() !== 'http' && scheme.toLowerCase() !== 'https') {
|
|
213
|
+
// Custom scheme (myapp://order/123): treat host + path as the route.
|
|
214
|
+
const rest = s.slice(scheme.length + 1).replace(/^\/\//, '');
|
|
215
|
+
const cut = rest.search(/[?#]/);
|
|
216
|
+
const pathPart = (cut === -1 ? rest : rest.slice(0, cut)).replace(/^\/+/, '');
|
|
217
|
+
const tail = cut === -1 ? '' : rest.slice(cut);
|
|
218
|
+
return '/' + pathPart + tail;
|
|
219
|
+
}
|
|
220
|
+
try { const u = new URL(url, window.location.origin); return u.pathname + u.search + u.hash; }
|
|
221
|
+
catch (e) { return '/'; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Faithful SPA navigation: route through the router's own click interceptor.
|
|
225
|
+
function manifestNavigateTo(path) {
|
|
226
|
+
const nav = window.ManifestRoutingNavigation;
|
|
227
|
+
if (nav) {
|
|
228
|
+
const a = document.createElement('a');
|
|
229
|
+
a.setAttribute('href', path);
|
|
230
|
+
document.body.appendChild(a);
|
|
231
|
+
a.click();
|
|
232
|
+
a.remove();
|
|
233
|
+
} else {
|
|
234
|
+
window.location.assign(path);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let _manifestLinkHandler = null;
|
|
239
|
+
function manifestHandleUrl(url) {
|
|
240
|
+
const path = manifestLinkPath(url);
|
|
241
|
+
try { const s = window.Alpine && window.Alpine.store('links'); if (s) s.last = url; } catch (e) {}
|
|
242
|
+
if (typeof _manifestLinkHandler === 'function') { try { _manifestLinkHandler({ url, path }); } catch (e) {} return; }
|
|
243
|
+
manifestNavigateTo(path);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function initManifestLinks() {
|
|
247
|
+
if (!window.Alpine.store('links')) window.Alpine.store('links', { last: null });
|
|
248
|
+
window.Alpine.magic('links', () => ({
|
|
249
|
+
on: (fn) => { _manifestLinkHandler = typeof fn === 'function' ? fn : null; },
|
|
250
|
+
open: (url) => manifestHandleUrl(url),
|
|
251
|
+
get last() { const s = window.Alpine.store('links'); return s ? s.last : null; }
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
const App = manifestNativePlugin('App');
|
|
255
|
+
if (App) {
|
|
256
|
+
try { App.addListener('appUrlOpen', (data) => { if (data && data.url) manifestHandleUrl(data.url); }); } catch (e) {}
|
|
257
|
+
try { App.getLaunchUrl().then(r => { if (r && r.url) manifestHandleUrl(r.url); }).catch(() => {}); } catch (e) {}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
// $push — push notifications (Capacitor PushNotifications → APNs/FCM), designed as
|
|
263
|
+
// three author-controlled contracts, not display-only:
|
|
264
|
+
// 1. permission timing — $push.request() (never auto-prompts on load)
|
|
265
|
+
// 2. device token — $push.onToken(fn): token -> your backend
|
|
266
|
+
// 3. tap -> route — tapped payload's url/route hands off to the router
|
|
267
|
+
// ($push.onTap(fn) to override; reuses $links + $app)
|
|
268
|
+
// Web fallback: permission maps to the Notification API; full web push (service
|
|
269
|
+
// worker + VAPID) is out of scope, so register() is a no-op on the web.
|
|
270
|
+
|
|
271
|
+
let _manifestPushTokenHandlers = [];
|
|
272
|
+
let _manifestPushReceiveHandlers = [];
|
|
273
|
+
let _manifestPushTapHandler = null;
|
|
274
|
+
|
|
275
|
+
function manifestPushMapPermission(p) {
|
|
276
|
+
if (p === 'granted' || p === 'denied' || p === 'prompt') return p;
|
|
277
|
+
if (p === 'default') return 'prompt';
|
|
278
|
+
return p || 'prompt';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function manifestPushRoutePayload(notification) {
|
|
282
|
+
const data = (notification && (notification.data || notification)) || {};
|
|
283
|
+
return data.url || data.route || data.path || null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function manifestPushHandleTap(notification) {
|
|
287
|
+
if (typeof _manifestPushTapHandler === 'function') { try { _manifestPushTapHandler(notification); } catch (e) {} return; }
|
|
288
|
+
const target = manifestPushRoutePayload(notification);
|
|
289
|
+
if (target && typeof manifestHandleUrl === 'function') manifestHandleUrl(target);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function initManifestPush() {
|
|
293
|
+
let store = window.Alpine.store('push');
|
|
294
|
+
if (!store) {
|
|
295
|
+
store = { permission: 'prompt', token: null };
|
|
296
|
+
try { store.permission = (typeof Notification !== 'undefined') ? manifestPushMapPermission(Notification.permission) : 'unsupported'; }
|
|
297
|
+
catch (e) { store.permission = 'unsupported'; }
|
|
298
|
+
window.Alpine.store('push', store);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const Push = manifestNativePlugin('PushNotifications');
|
|
302
|
+
if (Push) {
|
|
303
|
+
try { Push.addListener('registration', t => { store.token = t && t.value; _manifestPushTokenHandlers.forEach(fn => { try { fn(store.token); } catch (e) {} }); }); } catch (e) {}
|
|
304
|
+
try { Push.addListener('pushNotificationReceived', n => { _manifestPushReceiveHandlers.forEach(fn => { try { fn(n); } catch (e) {} }); }); } catch (e) {}
|
|
305
|
+
try { Push.addListener('pushNotificationActionPerformed', a => manifestPushHandleTap(a && a.notification)); } catch (e) {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
window.Alpine.magic('push', () => ({
|
|
309
|
+
async request() {
|
|
310
|
+
const P = manifestNativePlugin('PushNotifications');
|
|
311
|
+
if (P) { try { const r = await P.requestPermissions(); const p = manifestPushMapPermission(r && r.receive); window.Alpine.store('push').permission = p; return p; } catch (e) { return 'denied'; } }
|
|
312
|
+
if (typeof Notification !== 'undefined' && Notification.requestPermission) { try { const p = manifestPushMapPermission(await Notification.requestPermission()); window.Alpine.store('push').permission = p; return p; } catch (e) { return 'denied'; } }
|
|
313
|
+
return 'unsupported';
|
|
314
|
+
},
|
|
315
|
+
async register() {
|
|
316
|
+
const P = manifestNativePlugin('PushNotifications');
|
|
317
|
+
if (P) { try { await P.register(); } catch (e) {} }
|
|
318
|
+
},
|
|
319
|
+
onToken(fn) { if (typeof fn === 'function') _manifestPushTokenHandlers.push(fn); },
|
|
320
|
+
onReceive(fn) { if (typeof fn === 'function') _manifestPushReceiveHandlers.push(fn); },
|
|
321
|
+
onTap(fn) { _manifestPushTapHandler = typeof fn === 'function' ? fn : null; },
|
|
322
|
+
get permission() { const s = window.Alpine.store('push'); return s ? s.permission : 'prompt'; },
|
|
323
|
+
get token() { const s = window.Alpine.store('push'); return s ? s.token : null; }
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// $app — app lifecycle. Native: Capacitor App appStateChange (foreground/background).
|
|
329
|
+
// Web fallback: document visibilitychange + pageshow/pagehide. $app.active is
|
|
330
|
+
// reactive; onChange hooks fire on transition. Push delivers tapped-notification
|
|
331
|
+
// routing around resume, so this pairs with $push + $links.
|
|
332
|
+
|
|
333
|
+
let _manifestAppChangeHandlers = [];
|
|
334
|
+
|
|
335
|
+
function manifestAppSetActive(active) {
|
|
336
|
+
try { const s = window.Alpine && window.Alpine.store('app'); if (s) s.active = !!active; } catch (e) {}
|
|
337
|
+
_manifestAppChangeHandlers.forEach(fn => { try { fn(!!active); } catch (e) {} });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function initManifestApp() {
|
|
341
|
+
const visible = (typeof document !== 'undefined') ? document.visibilityState !== 'hidden' : true;
|
|
342
|
+
if (!window.Alpine.store('app')) window.Alpine.store('app', { active: visible });
|
|
343
|
+
|
|
344
|
+
const App = manifestNativePlugin('App');
|
|
345
|
+
if (App) {
|
|
346
|
+
try { App.addListener('appStateChange', s => manifestAppSetActive(s && s.isActive)); } catch (e) {}
|
|
347
|
+
} else if (typeof document !== 'undefined') {
|
|
348
|
+
document.addEventListener('visibilitychange', () => manifestAppSetActive(document.visibilityState !== 'hidden'));
|
|
349
|
+
window.addEventListener('pageshow', () => manifestAppSetActive(true));
|
|
350
|
+
window.addEventListener('pagehide', () => manifestAppSetActive(false));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
window.Alpine.magic('app', () => ({
|
|
354
|
+
get active() { const s = window.Alpine.store('app'); return s ? s.active : true; },
|
|
355
|
+
onChange(fn) { if (typeof fn === 'function') _manifestAppChangeHandlers.push(fn); }
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// $haptics — tactile feedback. Native: Capacitor Haptics. Web fallback:
|
|
361
|
+
// navigator.vibrate (limited; iOS Safari ignores it — fine, it's an enhancement).
|
|
362
|
+
// No-ops silently when unsupported.
|
|
363
|
+
|
|
364
|
+
function manifestVibrate(pattern) {
|
|
365
|
+
try { if (navigator.vibrate) return navigator.vibrate(pattern); } catch (e) {}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function initManifestHaptics() {
|
|
370
|
+
window.Alpine.magic('haptics', () => ({
|
|
371
|
+
impact(style) {
|
|
372
|
+
const H = manifestNativePlugin('Haptics');
|
|
373
|
+
if (H) { try { return H.impact({ style: style || 'MEDIUM' }); } catch (e) {} }
|
|
374
|
+
manifestVibrate(10); return Promise.resolve();
|
|
375
|
+
},
|
|
376
|
+
notification(type) {
|
|
377
|
+
const H = manifestNativePlugin('Haptics');
|
|
378
|
+
if (H) { try { return H.notification({ type: type || 'SUCCESS' }); } catch (e) {} }
|
|
379
|
+
manifestVibrate([10, 40, 10]); return Promise.resolve();
|
|
380
|
+
},
|
|
381
|
+
selection() {
|
|
382
|
+
const H = manifestNativePlugin('Haptics');
|
|
383
|
+
if (H) { try { return H.selectionChanged(); } catch (e) {} }
|
|
384
|
+
manifestVibrate(5); return Promise.resolve();
|
|
385
|
+
},
|
|
386
|
+
vibrate(ms) {
|
|
387
|
+
const H = manifestNativePlugin('Haptics');
|
|
388
|
+
if (H) { try { return H.vibrate({ duration: ms || 300 }); } catch (e) {} }
|
|
389
|
+
manifestVibrate(ms || 300); return Promise.resolve();
|
|
390
|
+
}
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
// $biometric — Face ID / Touch ID. Native: a Capacitor biometric plugin
|
|
396
|
+
// (BiometricAuth or NativeBiometric; method shapes differ, adapted below). The web
|
|
397
|
+
// has no equivalent one-shot verify (WebAuthn is a separate server ceremony), so
|
|
398
|
+
// available() is false and verify() reports unsupported — a genuine native win.
|
|
399
|
+
|
|
400
|
+
function manifestBiometricPlugin() {
|
|
401
|
+
return manifestNativePlugin('BiometricAuth') || manifestNativePlugin('NativeBiometric') || null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function manifestBiometricAvailable() {
|
|
405
|
+
const B = manifestBiometricPlugin();
|
|
406
|
+
if (!B) return false;
|
|
407
|
+
try {
|
|
408
|
+
if (typeof B.checkBiometry === 'function') { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
|
|
409
|
+
if (typeof B.isAvailable === 'function') { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
|
|
410
|
+
} catch (e) {}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function manifestBiometricVerify(opts) {
|
|
415
|
+
const o = opts || {};
|
|
416
|
+
const B = manifestBiometricPlugin();
|
|
417
|
+
if (!B) return { verified: false, error: 'unsupported' };
|
|
418
|
+
try {
|
|
419
|
+
if (typeof B.authenticate === 'function') { await B.authenticate({ reason: o.reason, title: o.title, subtitle: o.subtitle, cancelTitle: o.cancelTitle }); return { verified: true }; }
|
|
420
|
+
if (typeof B.verifyIdentity === 'function') { await B.verifyIdentity({ reason: o.reason, title: o.title, subtitle: o.subtitle }); return { verified: true }; }
|
|
421
|
+
} catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed' }; }
|
|
422
|
+
return { verified: false, error: 'unsupported' };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function initManifestBiometric() {
|
|
426
|
+
window.Alpine.magic('biometric', () => ({
|
|
427
|
+
available: () => manifestBiometricAvailable(),
|
|
428
|
+
verify: (opts) => manifestBiometricVerify(opts)
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
// $camera — capture or pick a photo. Native: Capacitor Camera.getPhoto. Web
|
|
434
|
+
// fallback: a file input (with capture hint on mobile) returning a data URL.
|
|
435
|
+
// Resolves { dataUrl?, format?, cancelled?, error? }.
|
|
436
|
+
|
|
437
|
+
function manifestCameraWeb(opts) {
|
|
438
|
+
const o = opts || {};
|
|
439
|
+
return new Promise(resolve => {
|
|
440
|
+
try {
|
|
441
|
+
const input = document.createElement('input');
|
|
442
|
+
input.type = 'file';
|
|
443
|
+
input.accept = 'image/*';
|
|
444
|
+
if (o.source !== 'photos') input.setAttribute('capture', 'environment');
|
|
445
|
+
input.style.position = 'fixed';
|
|
446
|
+
input.style.left = '-9999px';
|
|
447
|
+
let settled = false;
|
|
448
|
+
const done = (result) => {
|
|
449
|
+
if (settled) return;
|
|
450
|
+
settled = true;
|
|
451
|
+
window.removeEventListener('focus', onFocus);
|
|
452
|
+
input.remove();
|
|
453
|
+
resolve(result);
|
|
454
|
+
};
|
|
455
|
+
// No 'change' fires when the picker is cancelled; detect it on focus return.
|
|
456
|
+
const onFocus = () => setTimeout(() => { if (!settled && (!input.files || !input.files.length)) done({ cancelled: true }); }, 500);
|
|
457
|
+
input.addEventListener('change', () => {
|
|
458
|
+
const file = input.files && input.files[0];
|
|
459
|
+
if (!file) { done({ cancelled: true }); return; }
|
|
460
|
+
const reader = new FileReader();
|
|
461
|
+
reader.onload = () => done({ dataUrl: reader.result, format: (file.type.split('/')[1]) || '' });
|
|
462
|
+
reader.onerror = () => done({ cancelled: true, error: 'read-failed' });
|
|
463
|
+
reader.readAsDataURL(file);
|
|
464
|
+
});
|
|
465
|
+
window.addEventListener('focus', onFocus);
|
|
466
|
+
document.body.appendChild(input);
|
|
467
|
+
input.click();
|
|
468
|
+
} catch (e) { resolve({ cancelled: true, error: 'unsupported' }); }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function manifestCameraPhoto(opts) {
|
|
473
|
+
const o = opts || {};
|
|
474
|
+
const Cam = manifestNativePlugin('Camera');
|
|
475
|
+
if (Cam) {
|
|
476
|
+
return Cam.getPhoto({ resultType: 'dataUrl', source: o.source === 'photos' ? 'PHOTOS' : 'CAMERA', quality: o.quality || 90 })
|
|
477
|
+
.then(r => ({ dataUrl: r && (r.dataUrl || r.webPath), format: r && r.format }))
|
|
478
|
+
.catch(e => ({ cancelled: true, error: (e && (e.message || e.code)) || 'cancelled' }));
|
|
479
|
+
}
|
|
480
|
+
return manifestCameraWeb(o);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function initManifestCamera() {
|
|
484
|
+
window.Alpine.magic('camera', () => ({
|
|
485
|
+
photo: (opts) => manifestCameraPhoto(opts),
|
|
486
|
+
pick: (opts) => manifestCameraPhoto(Object.assign({}, opts || {}, { source: 'photos' }))
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
})();
|
package/lib/manifest.edit.js
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
if (theme) themeScopes[k] = el;
|
|
94
94
|
if (theme && !caps.size && !lock) return; // pure theme scope — a cascade target, NOT an editable area (so it doesn't swallow nested areas)
|
|
95
95
|
if (!caps.size && !lock) ['sort', 'text', 'style'].forEach(c => caps.add(c)); // size is opt-in
|
|
96
|
-
if ((expression || '').trim() && [...editEls].some(e => e._edit && e._edit.key === k)) console.warn('[edit] duplicate x-edit key (deltas will mis-route):', k);
|
|
96
|
+
if ((expression || '').trim() && [...editEls].some(e => e !== el && e._edit && e._edit.key === k)) console.warn('[edit] duplicate x-edit key (deltas will mis-route):', k); // re-init of the same element is not a duplicate
|
|
97
97
|
el._edit = { key: k, caps, lock, gated: modifiers.includes('gated'), theme, authoring: modifiers.includes('authoring') };
|
|
98
98
|
el.setAttribute('data-edit-area', '');
|
|
99
99
|
editEls.add(el);
|
|
@@ -1231,10 +1231,34 @@
|
|
|
1231
1231
|
}
|
|
1232
1232
|
|
|
1233
1233
|
/* ---- Activation: per-area, always-on by default (.gated needs $edit.on()) ---- */
|
|
1234
|
-
let estore;
|
|
1234
|
+
let estore, booted = false, armQueued = false;
|
|
1235
1235
|
const isActive = (a) => !!a && (!a._edit.gated || (estore && estore.active));
|
|
1236
1236
|
const anyActive = () => areas().some(isActive);
|
|
1237
|
-
function armAll() { areas().forEach(a => { if (isActive(a) && !a._armed) { lastSnap[key(a)] = snapshot(a, classify(a)); armArea(a); a._armed = true; } }); armThemeControls(); refresh(); }
|
|
1237
|
+
function armAll() { areas().forEach(a => { if (isActive(a) && !a._armed) { lastSnap[key(a)] = snapshot(a, classify(a)); armArea(a); a._armed = true; } }); armThemeControls(); refresh(); booted = true; }
|
|
1238
|
+
|
|
1239
|
+
/* ---- Late regions: arm when Alpine initialises them, release when it destroys them ---- */
|
|
1240
|
+
// Arming ran once at boot, so anything rendered later — x-if, a route change, a lazy
|
|
1241
|
+
// component, x-markdown output — stayed inert. Every one of those paths goes through
|
|
1242
|
+
// Alpine.initTree, so the x-edit directive itself is the hook; no observer needed.
|
|
1243
|
+
// Deferred one microtask so the tree Alpine is walking is complete before baselines
|
|
1244
|
+
// are captured, and batched so a burst of regions costs one pass.
|
|
1245
|
+
function armLater() {
|
|
1246
|
+
if (!booted || armQueued) return; // the boot pass still owns start-up ordering
|
|
1247
|
+
armQueued = true;
|
|
1248
|
+
queueMicrotask(() => { armQueued = false; armAll(); });
|
|
1249
|
+
}
|
|
1250
|
+
// Runs from Alpine's directive cleanup (x-if/x-for teardown, or its own mutation
|
|
1251
|
+
// observer), so a removed region drops its observers and its registry slot.
|
|
1252
|
+
function releaseEdit(el) {
|
|
1253
|
+
Object.keys(themeScopes).forEach(k => { if (themeScopes[k] === el) delete themeScopes[k]; });
|
|
1254
|
+
cssvarControls.delete(el);
|
|
1255
|
+
if (!editEls.has(el)) return;
|
|
1256
|
+
if (el._armed) { disarmArea(el); el._armed = false; }
|
|
1257
|
+
[el, ...el.querySelectorAll('*')].forEach(n => { if (n._sortObserver) { n._sortObserver.disconnect(); n._sortObserver = null; } });
|
|
1258
|
+
editEls.delete(el); delete el._edit;
|
|
1259
|
+
el.removeAttribute('data-edit-area');
|
|
1260
|
+
refresh();
|
|
1261
|
+
}
|
|
1238
1262
|
// Build B-side source patches from the edit set (storage-agnostic; overlay
|
|
1239
1263
|
// already auto-persists to localStorage on commit).
|
|
1240
1264
|
function buildPatches() {
|
|
@@ -1337,7 +1361,11 @@
|
|
|
1337
1361
|
function waitForData(area, cb) { const expr = dataSourceExpr(area), tpl = area.querySelector('template[x-for]'); let n = 0; const t = setInterval(() => { let r = false; try { r = Array.isArray(window.Alpine.evaluate(tpl, expr)); } catch {} if (r) { clearInterval(t); cb(); } else if (++n > 100) clearInterval(t); }, 50); }
|
|
1338
1362
|
function init() {
|
|
1339
1363
|
if (!window.Alpine || !Alpine.directive) return;
|
|
1340
|
-
Alpine.directive('edit', (el, { modifiers, expression }) =>
|
|
1364
|
+
Alpine.directive('edit', (el, { modifiers, expression }, { cleanup }) => {
|
|
1365
|
+
registerEdit(el, modifiers, expression);
|
|
1366
|
+
armLater(); // a region rendered after boot arms itself
|
|
1367
|
+
cleanup(() => releaseEdit(el)); // ...and lets go when Alpine tears it down
|
|
1368
|
+
});
|
|
1341
1369
|
Alpine.store('edit', {
|
|
1342
1370
|
active: false, canUndo: false, canRedo: false,
|
|
1343
1371
|
onPublish: null, // author sets a fn(patches, {log,cursor}) → route to cloud/custom
|
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
-
"manifest.appwrite.auth.js": "sha384-
|
|
3
|
-
"manifest.appwrite.data.js": "sha384-
|
|
2
|
+
"manifest.appwrite.auth.js": "sha384-HMJmRXnvdDBWyuteXL+DTxWM2llSCdjl4EDamEI7iHNAVlqeOCE4GDTm8ZHipPRj",
|
|
3
|
+
"manifest.appwrite.data.js": "sha384-avAqJJn65+6k2tbvGja/S0ZsEOtCEhFG93LgqODbuaP4kLaWFkVrZQwnnPJLxbIR",
|
|
4
4
|
"manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
|
|
5
5
|
"manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
|
|
6
6
|
"manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
|
|
7
|
-
"manifest.chat.js": "sha384-
|
|
7
|
+
"manifest.chat.js": "sha384-88DfMfJLoi2oxclVD2iIMDkIGAExMXq5k47mekUredlicLaP3oZY5bwtwqs+TqDF",
|
|
8
8
|
"manifest.code.js": "sha384-aoQ4E26vrhNe/+tL7K0h90c8/0ies7e8oIR6Ay92he2X3dnWZ+OPcn2Dna8fmc+c",
|
|
9
9
|
"manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
|
|
10
10
|
"manifest.colorpicker.js": "sha384-1+ZV5Hhjf7WnGP7b71Cqa5h3BKW8UgGT5V14i8Q+aF5zH8Xxwowt/ibSUVsiIHSm",
|
|
11
11
|
"manifest.combobox.js": "sha384-SN3XHumoebxcduFQqx5zt65/Cd10y8nuf9Gkco6UnsZ42sadRd+gvVLfWPdkkuPH",
|
|
12
|
-
"manifest.components.js": "sha384-
|
|
12
|
+
"manifest.components.js": "sha384-DLGSqCbnGkS87AWJ8GpKoAFP4nCosgC5SFSg6C10hH/nWu/MxI+0ntHlnFEfs++u",
|
|
13
13
|
"manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
|
|
14
|
-
"manifest.data.js": "sha384-
|
|
14
|
+
"manifest.data.js": "sha384-Cfww6fFCDaSckoFAUvLRf7ResiGMSmmLgNeZQLe3JbqHZufiwbhRMzTyw3FaOL0l",
|
|
15
15
|
"manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
|
|
16
|
-
"manifest.defer.js": "sha384-
|
|
16
|
+
"manifest.defer.js": "sha384-wXa60M2OHigi5rBurXQ7Oa8eyr5WTKs7EUn2GOru62KMdfs/LDyNegxCc9MNJFSe",
|
|
17
|
+
"manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
|
|
17
18
|
"manifest.dropdowns.js": "sha384-4HuHkBt4i8sTPUDLB9rNLli2/lWLY1tjtCLzey37gRwueG+FRLCnVH1q7ljiVvJR",
|
|
18
|
-
"manifest.edit.js": "sha384-
|
|
19
|
+
"manifest.edit.js": "sha384-oLsrH7/UkJ/KEl3UO77hexcf2rYCGgzXZd0fe2EMq3hKRE7aUqLiEnLJhSV7zTaY",
|
|
19
20
|
"manifest.export.js": "sha384-HCy3n6ifrcAGIyzTRgZ5JIRvSX9q9/8ykMKRSbt1QTnhXz6PgYlV0AE2N2Yi2re+",
|
|
20
21
|
"manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
|
|
21
22
|
"manifest.localization.js": "sha384-p8MRlfpjQuMar7rerGRjfdRKqIPoU/QOwVmnwN+PjhrlEZNpetYLZhqO2i5BZn9s",
|
|
22
23
|
"manifest.markdown.js": "sha384-HFE2VwBRX5vhDGQ3c8XgZ10Y5ENkXxHzgX9DjmIEffqLoUDbnJYNk3TFWT9k3e+f",
|
|
23
|
-
"manifest.native.js": "sha384-
|
|
24
|
-
"manifest.payments.js": "sha384-
|
|
24
|
+
"manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
|
|
25
|
+
"manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
|
|
25
26
|
"manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
|
|
26
27
|
"manifest.router.js": "sha384-4qmGEkRpgAo9xoQkW6+hHIhowvcyAggKB2J3Wf8tJtXp+L0hufoVAqzfhnqw4cNR",
|
|
27
28
|
"manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
|
|
28
|
-
"manifest.status.js": "sha384-
|
|
29
|
+
"manifest.status.js": "sha384-nwvUSbT+gpRMqhYx6MeBYswUMH72rY92mjgxv0vrSG7Ht7l83XYpEB6fCTbXjjfK",
|
|
29
30
|
"manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
|
|
30
|
-
"manifest.sw.js": "sha384-
|
|
31
|
+
"manifest.sw.js": "sha384-evmpNHyxUu1Qsu2FP7buzG48O5Ppbf0ZlCn8V4Br4LiU6qrNCkfqh1yXxuafEg6Z",
|
|
31
32
|
"manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
|
|
32
33
|
"manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
|
|
33
|
-
"manifest.text.edit.js": "sha384-
|
|
34
|
+
"manifest.text.edit.js": "sha384-pfhc4IPKNJokj6ldlgD+KwGLN4UoM21CNeVf2sVq/34X1Un2PUhEuyTg5itmtsXS",
|
|
34
35
|
"manifest.toasts.js": "sha384-/OV32LLMJl0Iw8NoNQJ+5lgntLxjIJq/YNj5JAKS/+nUhVx4MMB9h1FOqP7zuj2M",
|
|
35
36
|
"manifest.tooltips.js": "sha384-uXVVQADl6pY2Pj9i/UaSclZN7dzbKhLkmevv7Ad0+wXR8LgqodALNFrc5jZJjlwm",
|
|
36
37
|
"manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
|
|
37
38
|
"manifest.utilities.js": "sha384-RvUXxxMLv5q6Psng6jjfK5O1xw05mV6BdLjV92zQW7Zuy3XWctl3QnF5XYNuPyYq",
|
|
38
39
|
"manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
|
|
39
|
-
"manifest.js": "sha384
|
|
40
|
+
"manifest.js": "sha384-+rWocB3jT+iBErwbS1ZBwt24C/jhUavBduc5EzMSu8ZZooyO52LeGpW7UscLdLYx"
|
|
40
41
|
}
|
package/lib/manifest.js
CHANGED
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
const PLUGIN_DEPENDENCIES = {
|
|
262
262
|
'appwrite-data': ['data'],
|
|
263
263
|
'appwrite-presences': ['data', 'appwrite-auth'],
|
|
264
|
-
'
|
|
264
|
+
'device': ['utilities']
|
|
265
265
|
};
|
|
266
266
|
|
|
267
267
|
// Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
|
|
@@ -381,6 +381,7 @@
|
|
|
381
381
|
// entry means the fetch finished; a complete document has run every parser tag.
|
|
382
382
|
function scriptSettled(el) {
|
|
383
383
|
if (el.hasAttribute('data-mnfst-loaded')) return true;
|
|
384
|
+
if (el.hasAttribute('data-mnfst-loading')) return false; // ours, still fetching or executing: only its load event counts
|
|
384
385
|
if (document.readyState === 'complete') return true;
|
|
385
386
|
if (!el.async && !el.defer && loaderScript && (loaderScript.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)) return true;
|
|
386
387
|
try { if (el.src && performance.getEntriesByName(el.src).length) return true; } catch (_) { /* no resource timing */ }
|
|
@@ -406,8 +407,9 @@
|
|
|
406
407
|
const script = document.createElement('script');
|
|
407
408
|
script.src = url;
|
|
408
409
|
script.async = false; // Ensure scripts execute in order
|
|
409
|
-
script.
|
|
410
|
-
script.
|
|
410
|
+
script.setAttribute('data-mnfst-loading', '');
|
|
411
|
+
script.onload = () => { script.removeAttribute('data-mnfst-loading'); script.setAttribute('data-mnfst-loaded', ''); resolve(); };
|
|
412
|
+
script.onerror = () => { script.removeAttribute('data-mnfst-loading'); script.remove(); reject(new Error(`Failed to load ${url}`)); };
|
|
411
413
|
document.head.appendChild(script);
|
|
412
414
|
});
|
|
413
415
|
}
|
|
@@ -434,7 +436,9 @@
|
|
|
434
436
|
const resolved = [];
|
|
435
437
|
const added = new Set();
|
|
436
438
|
|
|
439
|
+
const PLUGIN_ALIASES = { native: 'device' }; // renamed 0.5.199; the old name keeps working
|
|
437
440
|
function addPluginWithDeps(plugin) {
|
|
441
|
+
plugin = PLUGIN_ALIASES[plugin] || plugin;
|
|
438
442
|
if (added.has(plugin)) return;
|
|
439
443
|
|
|
440
444
|
const deps = PLUGIN_DEPENDENCIES[plugin];
|
|
@@ -508,7 +512,8 @@
|
|
|
508
512
|
// must not pull plugins in.
|
|
509
513
|
const USAGE_PLUGINS = [
|
|
510
514
|
{ magic: '$chat', plugin: 'chat' },
|
|
511
|
-
{ magic: '$presence', plugin: 'appwrite-presences' }
|
|
515
|
+
{ magic: '$presence', plugin: 'appwrite-presences' },
|
|
516
|
+
...['$share', '$secure', '$links', '$push', '$app', '$haptics', '$biometric', '$camera'].map(magic => ({ magic, plugin: 'device' }))
|
|
512
517
|
];
|
|
513
518
|
function detectUsagePlugins(alreadyLoaded) {
|
|
514
519
|
const wanted = USAGE_PLUGINS.filter(u => !alreadyLoaded.includes(u.plugin));
|
|
@@ -540,11 +545,12 @@
|
|
|
540
545
|
return [...found];
|
|
541
546
|
}
|
|
542
547
|
|
|
543
|
-
// Detect the
|
|
544
|
-
//
|
|
548
|
+
// Detect the device plugin (, , , …). Auto-loaded inside a
|
|
549
|
+
// Capacitor container, or when a `device` (formerly `native`) block opts in on the web.
|
|
545
550
|
function detectNativePlugins(manifest) {
|
|
546
|
-
if (typeof window !== 'undefined' && window.Capacitor) return ['
|
|
547
|
-
|
|
551
|
+
if (typeof window !== 'undefined' && window.Capacitor) return ['device'];
|
|
552
|
+
const block = manifest && typeof manifest === 'object' ? (manifest.device ?? manifest.native) : null;
|
|
553
|
+
if (block && typeof block === 'object') return ['device'];
|
|
548
554
|
return [];
|
|
549
555
|
}
|
|
550
556
|
|
|
@@ -637,7 +643,7 @@
|
|
|
637
643
|
// Expose API
|
|
638
644
|
window.Manifest = {
|
|
639
645
|
loadPlugin: function (pluginName, version = DEFAULT_VERSION) {
|
|
640
|
-
const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', '
|
|
646
|
+
const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device'];
|
|
641
647
|
if (!allPlugins.includes(pluginName)) {
|
|
642
648
|
console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
|
|
643
649
|
return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
|
|
@@ -710,6 +716,7 @@
|
|
|
710
716
|
const devServer = !!window.__mnfstRun;
|
|
711
717
|
const devOrigin = isDevHost(loc.hostname);
|
|
712
718
|
let manifest = window.__manifestLoaded || null;
|
|
719
|
+
if (!manifest && window.__manifestPromise) manifest = await window.__manifestPromise.catch(() => null);
|
|
713
720
|
if (!manifest) {
|
|
714
721
|
const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
|
|
715
722
|
manifest = await fetch(url).then(r => r.ok ? r.json() : null).catch(() => null);
|
|
@@ -829,13 +836,26 @@
|
|
|
829
836
|
walk(obj);
|
|
830
837
|
};
|
|
831
838
|
|
|
839
|
+
// One manifest.json request per boot: published on window the moment it starts so
|
|
840
|
+
// plugins that init before __manifestLoaded is set (data, auth, components) await it
|
|
841
|
+
// instead of fetching their own. Resolves interpolated, or null.
|
|
842
|
+
const shareManifestFetch = () => {
|
|
843
|
+
if (window.__manifestLoaded) return Promise.resolve(window.__manifestLoaded);
|
|
844
|
+
if (!window.__manifestPromise) {
|
|
845
|
+
window.__manifestPromise = fetch(manifestUrl).then(r => r.ok ? r.json() : null)
|
|
846
|
+
.then(m => { if (m && !m.__interpolated) { interpolateManifestEnv(m); Object.defineProperty(m, '__interpolated', { value: true, enumerable: false }); } return m; })
|
|
847
|
+
.catch(() => null);
|
|
848
|
+
}
|
|
849
|
+
return window.__manifestPromise;
|
|
850
|
+
};
|
|
851
|
+
|
|
832
852
|
const loadPlugins = async () => {
|
|
833
853
|
let manifest = null;
|
|
834
854
|
let pluginsToLoad = config.plugins;
|
|
835
855
|
let manifestPromise = null;
|
|
836
856
|
|
|
837
857
|
if (config.deriveFromManifest) {
|
|
838
|
-
manifest = await
|
|
858
|
+
manifest = await shareManifestFetch();
|
|
839
859
|
const corePlugins = getDefaultPluginsFromManifest(manifest);
|
|
840
860
|
const appwritePlugins = detectAppwritePlugins(manifest);
|
|
841
861
|
const paymentsPlugins = detectPaymentsPlugins(manifest);
|
|
@@ -846,12 +866,12 @@
|
|
|
846
866
|
pluginsToLoad = resolveDependencies([...ALWAYS_ON, ...pluginsToLoad.filter(p => !ALWAYS_ON.includes(p))]);
|
|
847
867
|
const needsManifest = config.plugins.some(p => MANIFEST_DEPENDENT_PLUGINS.includes(p));
|
|
848
868
|
if (needsManifest) {
|
|
849
|
-
manifestPromise =
|
|
869
|
+
manifestPromise = shareManifestFetch();
|
|
850
870
|
}
|
|
851
871
|
// Inside a Capacitor container, ensure the native umbrella loads even on
|
|
852
872
|
// the explicit data-plugins path (matches the derive-path auto-inject).
|
|
853
|
-
if (typeof window !== 'undefined' && window.Capacitor && !pluginsToLoad.includes('
|
|
854
|
-
pluginsToLoad = resolveDependencies([...pluginsToLoad, '
|
|
873
|
+
if (typeof window !== 'undefined' && window.Capacitor && !pluginsToLoad.includes('device')) {
|
|
874
|
+
pluginsToLoad = resolveDependencies([...pluginsToLoad, 'device']);
|
|
855
875
|
}
|
|
856
876
|
}
|
|
857
877
|
|
|
@@ -868,9 +888,7 @@
|
|
|
868
888
|
manifest = await manifestPromise;
|
|
869
889
|
}
|
|
870
890
|
if (manifest && typeof window !== 'undefined') {
|
|
871
|
-
//
|
|
872
|
-
// appwrite-auth etc. read window.__manifestLoaded directly.
|
|
873
|
-
interpolateManifestEnv(manifest);
|
|
891
|
+
// Already interpolated by shareManifestFetch; appwrite-auth etc. read window.__manifestLoaded directly.
|
|
874
892
|
window.__manifestLoaded = manifest;
|
|
875
893
|
if (window.ManifestComponentsRegistry) {
|
|
876
894
|
window.ManifestComponentsRegistry.manifest = manifest;
|
package/lib/manifest.native.js
CHANGED
package/lib/manifest.payments.js
CHANGED
|
@@ -21,10 +21,11 @@ function resolvedOrNull(value, fieldName) {
|
|
|
21
21
|
async function ensureManifest() {
|
|
22
22
|
if (window.ManifestComponentsRegistry?.manifest) return window.ManifestComponentsRegistry.manifest;
|
|
23
23
|
if (window.__manifestLoaded) return window.__manifestLoaded;
|
|
24
|
+
if (window.__manifestPromise) { const shared = await window.__manifestPromise.catch(() => null); if (shared) return shared; }
|
|
24
25
|
try {
|
|
25
26
|
const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
|
|
26
|
-
|
|
27
|
-
return await
|
|
27
|
+
window.__manifestPromise = fetch(url).then(r => r.json());
|
|
28
|
+
return await window.__manifestPromise;
|
|
28
29
|
} catch (_) {
|
|
29
30
|
return null;
|
|
30
31
|
}
|
package/lib/manifest.schema.json
CHANGED
|
@@ -408,9 +408,14 @@
|
|
|
408
408
|
},
|
|
409
409
|
"additionalProperties": true
|
|
410
410
|
},
|
|
411
|
+
"device": {
|
|
412
|
+
"type": "object",
|
|
413
|
+
"description": "Opts the app into the device plugin: $share, $secure, $links, $push, $app, $haptics, $biometric and $camera, each with a web fallback and Capacitor as the native adapter. Auto-loaded inside a Capacitor container and whenever a page uses one of those magics; an empty object loads it on every build.",
|
|
414
|
+
"additionalProperties": true
|
|
415
|
+
},
|
|
411
416
|
"native": {
|
|
412
417
|
"type": "object",
|
|
413
|
-
"description": "
|
|
418
|
+
"description": "Deprecated alias of `device` (renamed in 0.5.199); still accepted.",
|
|
414
419
|
"additionalProperties": true
|
|
415
420
|
}
|
|
416
421
|
},
|
package/lib/manifest.status.js
CHANGED
|
@@ -64,13 +64,14 @@
|
|
|
64
64
|
// cache a normalized copy that omits keys they don't consume.
|
|
65
65
|
async function ensureStatusManifest() {
|
|
66
66
|
const cached = window.ManifestComponentsRegistry?.manifest || window.__manifestLoaded;
|
|
67
|
-
if (cached
|
|
67
|
+
if (cached) return cached; // a loaded manifest without `status` means no status config, not a stale copy
|
|
68
|
+
if (window.__manifestPromise) { const shared = await window.__manifestPromise.catch(() => null); if (shared) return shared; }
|
|
68
69
|
try {
|
|
69
70
|
const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
|
|
70
|
-
|
|
71
|
-
return await
|
|
71
|
+
window.__manifestPromise = fetch(url).then(r => r.json());
|
|
72
|
+
return await window.__manifestPromise;
|
|
72
73
|
} catch (_) {
|
|
73
|
-
return
|
|
74
|
+
return null;
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
package/lib/manifest.sw.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
// Build stamps the published version here; unstamped (dev) falls back to the
|
|
18
18
|
// registration URL's `v`, then 'dev'.
|
|
19
|
-
const BUILD_VERSION = '0.5.199-next.
|
|
19
|
+
const BUILD_VERSION = '0.5.199-next.4';
|
|
20
20
|
const CACHE_PREFIX = 'mnfst-sw:';
|
|
21
21
|
const META_CACHE = CACHE_PREFIX + 'meta';
|
|
22
22
|
const SWR_CAP = 200;
|
|
@@ -1563,9 +1563,9 @@
|
|
|
1563
1563
|
// Letting go of the target has to be announced. A menu anchored to the selection
|
|
1564
1564
|
// is watching for it, and without a final null it hangs about over content it no
|
|
1565
1565
|
// longer has anything to do with.
|
|
1566
|
-
function release() {
|
|
1567
|
-
const was = lastFocused;
|
|
1568
|
-
lastFocused = null;
|
|
1566
|
+
function release(area) {
|
|
1567
|
+
const was = area || lastFocused;
|
|
1568
|
+
if (!area || area === lastFocused) lastFocused = null;
|
|
1569
1569
|
if (!was) return;
|
|
1570
1570
|
was._selection = null;
|
|
1571
1571
|
was.removeAttribute('data-text-edit-selected');
|
|
@@ -1573,6 +1573,19 @@
|
|
|
1573
1573
|
was.dispatchEvent(new CustomEvent('text-edit:selection', { bubbles: true, detail: null }));
|
|
1574
1574
|
}
|
|
1575
1575
|
|
|
1576
|
+
// relatedTarget is null for a click onto plain page text; the pointer that caused
|
|
1577
|
+
// the blur stands in, and only for the tick that dispatched it.
|
|
1578
|
+
let pointerTarget = null;
|
|
1579
|
+
|
|
1580
|
+
// Focus is still the area's own, or on a control that acts on it.
|
|
1581
|
+
function keeps(area, node) {
|
|
1582
|
+
const el = elementAt(node);
|
|
1583
|
+
if (!el) return false;
|
|
1584
|
+
if (area.contains(el)) return true;
|
|
1585
|
+
const control = el.closest('[data-text-edit-control]');
|
|
1586
|
+
return !!control && resolve(control) === area;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1576
1589
|
const editableElsewhere = (node) => {
|
|
1577
1590
|
const el = elementAt(node);
|
|
1578
1591
|
return !!el && (el.isContentEditable || /^(INPUT|TEXTAREA)$/.test(el.tagName));
|
|
@@ -1650,6 +1663,19 @@
|
|
|
1650
1663
|
sync();
|
|
1651
1664
|
}
|
|
1652
1665
|
|
|
1666
|
+
// A link at the caret comes off; a selection gets one, through the area's own url
|
|
1667
|
+
// control where there is one.
|
|
1668
|
+
function toggleLink(area) {
|
|
1669
|
+
if (ancestor('a')) { run(area, 'a'); return; }
|
|
1670
|
+
if (!hasSelection()) return;
|
|
1671
|
+
saveRange(area);
|
|
1672
|
+
const field = [...controls].find(c => c._te && c._te.id === 'a' && !c._te.page
|
|
1673
|
+
&& 'value' in c && c.tagName !== 'BUTTON' && resolve(c) === area);
|
|
1674
|
+
if (field) { field.focus(); if (field.select) field.select(); return; }
|
|
1675
|
+
const url = typeof window.prompt === 'function' ? window.prompt('Link URL', '') : null;
|
|
1676
|
+
if (url) run(area, 'a', url);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1653
1679
|
// Reflect caret state onto every control pointing at the area that owns it.
|
|
1654
1680
|
function sync() {
|
|
1655
1681
|
controls.forEach(c => {
|
|
@@ -1695,10 +1721,18 @@
|
|
|
1695
1721
|
};
|
|
1696
1722
|
});
|
|
1697
1723
|
|
|
1724
|
+
document.addEventListener('pointerdown', (e) => {
|
|
1725
|
+
pointerTarget = e.target;
|
|
1726
|
+
setTimeout(() => { pointerTarget = null; }, 0);
|
|
1727
|
+
}, true);
|
|
1728
|
+
|
|
1698
1729
|
document.addEventListener('selectionchange', () => {
|
|
1699
1730
|
const r = range(); if (!r) return;
|
|
1700
1731
|
const a = [...areas].find(x => x.contains(r.startContainer));
|
|
1701
|
-
if (a) {
|
|
1732
|
+
if (a) {
|
|
1733
|
+
if (lastFocused && lastFocused !== a) release(lastFocused); // the one being left owes a null
|
|
1734
|
+
saveRange(a); lastFocused = a; report(a);
|
|
1735
|
+
}
|
|
1702
1736
|
else if (lastFocused && editableElsewhere(r.startContainer)) release();
|
|
1703
1737
|
sync();
|
|
1704
1738
|
});
|
|
@@ -1894,6 +1928,12 @@
|
|
|
1894
1928
|
});
|
|
1895
1929
|
|
|
1896
1930
|
el.addEventListener('focusin', () => { lastFocused = el; sync(); });
|
|
1931
|
+
// Focus gone somewhere that is neither this area nor one of its controls.
|
|
1932
|
+
el.addEventListener('focusout', (e) => {
|
|
1933
|
+
if (keeps(el, e.relatedTarget || pointerTarget)) return;
|
|
1934
|
+
release(el);
|
|
1935
|
+
sync();
|
|
1936
|
+
});
|
|
1897
1937
|
el.addEventListener('input', (e) => {
|
|
1898
1938
|
if (!writing && e.data && applyPending(el, e.data)) { commitValue(); }
|
|
1899
1939
|
settle(); sync();
|
|
@@ -1967,7 +2007,8 @@
|
|
|
1967
2007
|
const id = { b: 'strong', i: 'em', u: 'u', k: 'a' }[e.key.toLowerCase()];
|
|
1968
2008
|
if (!id || !allows(el, id)) return;
|
|
1969
2009
|
e.preventDefault();
|
|
1970
|
-
|
|
2010
|
+
if (id === 'a') { toggleLink(el); return; }
|
|
2011
|
+
run(el, id);
|
|
1971
2012
|
});
|
|
1972
2013
|
|
|
1973
2014
|
// Paste as our own subset — never the source app's markup.
|