mnfst 0.5.171 → 0.5.172

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.
@@ -0,0 +1,484 @@
1
+ // Manifest Native — umbrella core: Capacitor detection + $device enrichment.
2
+ // Capacitor injects window.Capacitor (+ window.Capacitor.Plugins.<Name>) in the
3
+ // native container. Every capability degrades to a web equivalent when absent,
4
+ // so this plugin is safe to load on the web build too.
5
+
6
+ function manifestNativeIsNative() {
7
+ const cap = window.Capacitor;
8
+ return !!(cap && (typeof cap.isNativePlatform !== 'function' || cap.isNativePlatform()));
9
+ }
10
+ function manifestNativePlugin(name) {
11
+ const cap = window.Capacitor;
12
+ return (cap && cap.Plugins && cap.Plugins[name]) || null;
13
+ }
14
+ function manifestNativePlatform() {
15
+ const cap = window.Capacitor;
16
+ if (cap && typeof cap.getPlatform === 'function') { try { return cap.getPlatform(); } catch (e) {} }
17
+ return manifestNativeIsNative() ? (document.documentElement.getAttribute('data-os') || 'native') : 'web';
18
+ }
19
+
20
+ // Authoritatively stamp container/platform for $device + CSS variants.
21
+ function manifestNativeStamp() {
22
+ try {
23
+ if (!manifestNativeIsNative()) return;
24
+ const html = document.documentElement;
25
+ html.setAttribute('data-native', '');
26
+ const p = manifestNativePlatform();
27
+ if (p && p !== 'web') html.setAttribute('data-platform', p);
28
+ } catch (e) {}
29
+ }
30
+ manifestNativeStamp();
31
+
32
+ // Register capability magics once Alpine is available.
33
+ let manifestNativeInitialized = false;
34
+ function initManifestNative() {
35
+ // Network runs here (not at load) so the Capacitor Network reading can reach
36
+ // the $device store, which utilities registers on alpine:init.
37
+ if (typeof initManifestNativeNetwork === 'function') initManifestNativeNetwork();
38
+ if (typeof initManifestShare === 'function') initManifestShare();
39
+ if (typeof initManifestSecure === 'function') initManifestSecure();
40
+ if (typeof initManifestLinks === 'function') initManifestLinks();
41
+ if (typeof initManifestPush === 'function') initManifestPush();
42
+ if (typeof initManifestApp === 'function') initManifestApp();
43
+ if (typeof initManifestHaptics === 'function') initManifestHaptics();
44
+ if (typeof initManifestBiometric === 'function') initManifestBiometric();
45
+ if (typeof initManifestCamera === 'function') initManifestCamera();
46
+ }
47
+ function ensureManifestNativeInitialized() {
48
+ if (manifestNativeInitialized) return;
49
+ if (!window.Alpine || typeof window.Alpine.magic !== 'function') return;
50
+ manifestNativeInitialized = true;
51
+ initManifestNative();
52
+ }
53
+ window.ensureManifestNativeInitialized = ensureManifestNativeInitialized;
54
+ document.addEventListener('alpine:init', ensureManifestNativeInitialized);
55
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
56
+ setTimeout(ensureManifestNativeInitialized, 0);
57
+ } else {
58
+ const manifestNativeCheck = setInterval(() => {
59
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
60
+ clearInterval(manifestNativeCheck);
61
+ ensureManifestNativeInitialized();
62
+ }
63
+ }, 10);
64
+ setTimeout(() => clearInterval(manifestNativeCheck), 5000);
65
+ }
66
+
67
+
68
+ // $share — native share sheet (Capacitor Share) → Web Share API → clipboard.
69
+ // Resolves to { shared, method: 'native'|'web'|'clipboard'|'none', cancelled? }.
70
+
71
+ function manifestShareCancelled(e) {
72
+ return !!(e && (e.code === 'CANCELED' || e.name === 'AbortError' || /cancel|abort/i.test(e.message || '')));
73
+ }
74
+ function manifestShareClipboard(payload) {
75
+ const text = payload.url || payload.text || payload.title || '';
76
+ if (text && navigator.clipboard && navigator.clipboard.writeText) {
77
+ return navigator.clipboard.writeText(text)
78
+ .then(() => ({ shared: true, method: 'clipboard' }))
79
+ .catch(() => ({ shared: false, method: 'none' }));
80
+ }
81
+ return Promise.resolve({ shared: false, method: 'none' });
82
+ }
83
+ function manifestShareWeb(payload) {
84
+ if (navigator.share) {
85
+ return navigator.share({ title: payload.title, text: payload.text, url: payload.url })
86
+ .then(() => ({ shared: true, method: 'web' }))
87
+ .catch(e => manifestShareCancelled(e)
88
+ ? { shared: false, method: 'web', cancelled: true }
89
+ : manifestShareClipboard(payload));
90
+ }
91
+ return manifestShareClipboard(payload);
92
+ }
93
+ function manifestShare(opts) {
94
+ const payload = opts || {};
95
+ const Share = manifestNativePlugin('Share');
96
+ if (Share) {
97
+ // When the native sheet is present, never fall through to the web sheet
98
+ // (that would double-prompt); report the native outcome instead.
99
+ return Share.share({ title: payload.title, text: payload.text, url: payload.url, dialogTitle: payload.dialogTitle })
100
+ .then(() => ({ shared: true, method: 'native' }))
101
+ .catch(e => manifestShareCancelled(e)
102
+ ? { shared: false, method: 'native', cancelled: true }
103
+ : { shared: false, method: 'native', error: (e && (e.message || e.code)) || 'failed' });
104
+ }
105
+ return manifestShareWeb(payload);
106
+ }
107
+ function initManifestShare() {
108
+ window.Alpine.magic('share', () => manifestShare);
109
+ }
110
+
111
+
112
+ // Network — higher-fidelity connectivity via Capacitor Network, feeding $device.
113
+ // On the web the base $device signal already tracks navigator online/offline;
114
+ // this only upgrades fidelity inside the native container.
115
+
116
+ function manifestNativeApplyOnline(connected) {
117
+ const online = connected !== false;
118
+ try {
119
+ document.documentElement.setAttribute('data-online', online ? 'true' : 'false');
120
+ if (window.Alpine && typeof window.Alpine.store === 'function') {
121
+ const store = window.Alpine.store('device');
122
+ if (store) store.online = online;
123
+ }
124
+ } catch (e) {}
125
+ }
126
+ function initManifestNativeNetwork() {
127
+ const Network = manifestNativePlugin('Network');
128
+ if (!Network) return;
129
+ try { Network.getStatus().then(s => manifestNativeApplyOnline(s && s.connected)).catch(() => {}); } catch (e) {}
130
+ try { Network.addListener('networkStatusChange', s => manifestNativeApplyOnline(s && s.connected)); } catch (e) {}
131
+ }
132
+
133
+
134
+ // $secure — Keychain/Keystore-backed key/value storage (native) with a
135
+ // namespaced localStorage fallback on the web. Intended for session tokens etc.
136
+ // Native: install a Capacitor secure-storage plugin registered as
137
+ // window.Capacitor.Plugins.SecureStorage; if its method shape differs from the
138
+ // adapters below, override with $secure.use(adapter). Verify on device.
139
+ // The web fallback is NOT encrypted — it degrades honestly, not securely.
140
+
141
+ const MANIFEST_SECURE_NS = 'mnfst:';
142
+
143
+ function manifestSecureWebBackend() {
144
+ const ls = () => window.localStorage;
145
+ return {
146
+ async get(key) { try { return ls().getItem(MANIFEST_SECURE_NS + key); } catch (e) { return null; } },
147
+ async set(key, value) { try { ls().setItem(MANIFEST_SECURE_NS + key, String(value)); } catch (e) {} },
148
+ async remove(key) { try { ls().removeItem(MANIFEST_SECURE_NS + key); } catch (e) {} },
149
+ async keys() {
150
+ try { return Object.keys(ls()).filter(k => k.indexOf(MANIFEST_SECURE_NS) === 0).map(k => k.slice(MANIFEST_SECURE_NS.length)); }
151
+ catch (e) { return []; }
152
+ },
153
+ async clear() { try { const ks = await this.keys(); ks.forEach(k => ls().removeItem(MANIFEST_SECURE_NS + k)); } catch (e) {} }
154
+ };
155
+ }
156
+
157
+ // Adapts the two common Capacitor secure-storage plugin shapes: localStorage-like
158
+ // (getItem/setItem/removeItem) or object-arg (get/set/remove with {key,value}).
159
+ function manifestSecureNativeBackend(plugin) {
160
+ const itemApi = typeof plugin.getItem === 'function';
161
+ return {
162
+ async get(key) {
163
+ try {
164
+ if (itemApi) { const v = await plugin.getItem(key); return v == null ? null : v; }
165
+ const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
166
+ } catch (e) { return null; }
167
+ },
168
+ async set(key, value) {
169
+ try { if (itemApi) await plugin.setItem(key, String(value)); else await plugin.set({ key, value: String(value) }); } catch (e) {}
170
+ },
171
+ async remove(key) {
172
+ try { if (typeof plugin.removeItem === 'function') await plugin.removeItem(key); else await plugin.remove({ key }); } catch (e) {}
173
+ },
174
+ async keys() { try { const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || []; } catch (e) { return []; } },
175
+ async clear() { try { await plugin.clear(); } catch (e) {} }
176
+ };
177
+ }
178
+
179
+ let _manifestSecureBackend = null;
180
+ function manifestSecureBackend() {
181
+ if (_manifestSecureBackend) return _manifestSecureBackend;
182
+ const plugin = manifestNativePlugin('SecureStorage');
183
+ _manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin) : manifestSecureWebBackend();
184
+ return _manifestSecureBackend;
185
+ }
186
+
187
+ function initManifestSecure() {
188
+ window.Alpine.magic('secure', () => ({
189
+ get: (k) => manifestSecureBackend().get(k),
190
+ set: (k, v) => manifestSecureBackend().set(k, v),
191
+ remove: (k) => manifestSecureBackend().remove(k),
192
+ keys: () => manifestSecureBackend().keys(),
193
+ clear: () => manifestSecureBackend().clear(),
194
+ use: (adapter) => { _manifestSecureBackend = adapter; }
195
+ }));
196
+ }
197
+
198
+
199
+ // $links — deep / universal links. Native: Capacitor App fires appUrlOpen (and a
200
+ // cold-start launch URL); Manifest extracts the path and hands off to the router.
201
+ // $links.on(fn) lets the author take over routing (e.g. load data for /order/123).
202
+ // On the web there's no bridge, but $links.open(url) still works for programmatic
203
+ // deep-linking and testing. Couples with lifecycle + push (tap → route).
204
+
205
+ function manifestLinkPath(url) {
206
+ const s = String(url);
207
+ const scheme = (s.match(/^([a-z][a-z0-9+.-]*):/i) || [])[1];
208
+ if (scheme && scheme.toLowerCase() !== 'http' && scheme.toLowerCase() !== 'https') {
209
+ // Custom scheme (myapp://order/123): treat host + path as the route.
210
+ const rest = s.slice(scheme.length + 1).replace(/^\/\//, '');
211
+ const cut = rest.search(/[?#]/);
212
+ const pathPart = (cut === -1 ? rest : rest.slice(0, cut)).replace(/^\/+/, '');
213
+ const tail = cut === -1 ? '' : rest.slice(cut);
214
+ return '/' + pathPart + tail;
215
+ }
216
+ try { const u = new URL(url, window.location.origin); return u.pathname + u.search + u.hash; }
217
+ catch (e) { return '/'; }
218
+ }
219
+
220
+ // Faithful SPA navigation: route through the router's own click interceptor.
221
+ function manifestNavigateTo(path) {
222
+ const nav = window.ManifestRoutingNavigation;
223
+ if (nav) {
224
+ const a = document.createElement('a');
225
+ a.setAttribute('href', path);
226
+ document.body.appendChild(a);
227
+ a.click();
228
+ a.remove();
229
+ } else {
230
+ window.location.assign(path);
231
+ }
232
+ }
233
+
234
+ let _manifestLinkHandler = null;
235
+ function manifestHandleUrl(url) {
236
+ const path = manifestLinkPath(url);
237
+ try { const s = window.Alpine && window.Alpine.store('links'); if (s) s.last = url; } catch (e) {}
238
+ if (typeof _manifestLinkHandler === 'function') { try { _manifestLinkHandler({ url, path }); } catch (e) {} return; }
239
+ manifestNavigateTo(path);
240
+ }
241
+
242
+ function initManifestLinks() {
243
+ if (!window.Alpine.store('links')) window.Alpine.store('links', { last: null });
244
+ window.Alpine.magic('links', () => ({
245
+ on: (fn) => { _manifestLinkHandler = typeof fn === 'function' ? fn : null; },
246
+ open: (url) => manifestHandleUrl(url),
247
+ get last() { const s = window.Alpine.store('links'); return s ? s.last : null; }
248
+ }));
249
+
250
+ const App = manifestNativePlugin('App');
251
+ if (App) {
252
+ try { App.addListener('appUrlOpen', (data) => { if (data && data.url) manifestHandleUrl(data.url); }); } catch (e) {}
253
+ try { App.getLaunchUrl().then(r => { if (r && r.url) manifestHandleUrl(r.url); }).catch(() => {}); } catch (e) {}
254
+ }
255
+ }
256
+
257
+
258
+ // $push — push notifications (Capacitor PushNotifications → APNs/FCM), designed as
259
+ // three author-controlled contracts, not display-only:
260
+ // 1. permission timing — $push.request() (never auto-prompts on load)
261
+ // 2. device token — $push.onToken(fn): token -> your backend
262
+ // 3. tap -> route — tapped payload's url/route hands off to the router
263
+ // ($push.onTap(fn) to override; reuses $links + $app)
264
+ // Web fallback: permission maps to the Notification API; full web push (service
265
+ // worker + VAPID) is out of scope, so register() is a no-op on the web.
266
+
267
+ let _manifestPushTokenHandlers = [];
268
+ let _manifestPushReceiveHandlers = [];
269
+ let _manifestPushTapHandler = null;
270
+
271
+ function manifestPushMapPermission(p) {
272
+ if (p === 'granted' || p === 'denied' || p === 'prompt') return p;
273
+ if (p === 'default') return 'prompt';
274
+ return p || 'prompt';
275
+ }
276
+
277
+ function manifestPushRoutePayload(notification) {
278
+ const data = (notification && (notification.data || notification)) || {};
279
+ return data.url || data.route || data.path || null;
280
+ }
281
+
282
+ function manifestPushHandleTap(notification) {
283
+ if (typeof _manifestPushTapHandler === 'function') { try { _manifestPushTapHandler(notification); } catch (e) {} return; }
284
+ const target = manifestPushRoutePayload(notification);
285
+ if (target && typeof manifestHandleUrl === 'function') manifestHandleUrl(target);
286
+ }
287
+
288
+ function initManifestPush() {
289
+ let store = window.Alpine.store('push');
290
+ if (!store) {
291
+ store = { permission: 'prompt', token: null };
292
+ try { store.permission = (typeof Notification !== 'undefined') ? manifestPushMapPermission(Notification.permission) : 'unsupported'; }
293
+ catch (e) { store.permission = 'unsupported'; }
294
+ window.Alpine.store('push', store);
295
+ }
296
+
297
+ const Push = manifestNativePlugin('PushNotifications');
298
+ if (Push) {
299
+ try { Push.addListener('registration', t => { store.token = t && t.value; _manifestPushTokenHandlers.forEach(fn => { try { fn(store.token); } catch (e) {} }); }); } catch (e) {}
300
+ try { Push.addListener('pushNotificationReceived', n => { _manifestPushReceiveHandlers.forEach(fn => { try { fn(n); } catch (e) {} }); }); } catch (e) {}
301
+ try { Push.addListener('pushNotificationActionPerformed', a => manifestPushHandleTap(a && a.notification)); } catch (e) {}
302
+ }
303
+
304
+ window.Alpine.magic('push', () => ({
305
+ async request() {
306
+ const P = manifestNativePlugin('PushNotifications');
307
+ 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'; } }
308
+ 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'; } }
309
+ return 'unsupported';
310
+ },
311
+ async register() {
312
+ const P = manifestNativePlugin('PushNotifications');
313
+ if (P) { try { await P.register(); } catch (e) {} }
314
+ },
315
+ onToken(fn) { if (typeof fn === 'function') _manifestPushTokenHandlers.push(fn); },
316
+ onReceive(fn) { if (typeof fn === 'function') _manifestPushReceiveHandlers.push(fn); },
317
+ onTap(fn) { _manifestPushTapHandler = typeof fn === 'function' ? fn : null; },
318
+ get permission() { const s = window.Alpine.store('push'); return s ? s.permission : 'prompt'; },
319
+ get token() { const s = window.Alpine.store('push'); return s ? s.token : null; }
320
+ }));
321
+ }
322
+
323
+
324
+ // $app — app lifecycle. Native: Capacitor App appStateChange (foreground/background).
325
+ // Web fallback: document visibilitychange + pageshow/pagehide. $app.active is
326
+ // reactive; onChange hooks fire on transition. Push delivers tapped-notification
327
+ // routing around resume, so this pairs with $push + $links.
328
+
329
+ let _manifestAppChangeHandlers = [];
330
+
331
+ function manifestAppSetActive(active) {
332
+ try { const s = window.Alpine && window.Alpine.store('app'); if (s) s.active = !!active; } catch (e) {}
333
+ _manifestAppChangeHandlers.forEach(fn => { try { fn(!!active); } catch (e) {} });
334
+ }
335
+
336
+ function initManifestApp() {
337
+ const visible = (typeof document !== 'undefined') ? document.visibilityState !== 'hidden' : true;
338
+ if (!window.Alpine.store('app')) window.Alpine.store('app', { active: visible });
339
+
340
+ const App = manifestNativePlugin('App');
341
+ if (App) {
342
+ try { App.addListener('appStateChange', s => manifestAppSetActive(s && s.isActive)); } catch (e) {}
343
+ } else if (typeof document !== 'undefined') {
344
+ document.addEventListener('visibilitychange', () => manifestAppSetActive(document.visibilityState !== 'hidden'));
345
+ window.addEventListener('pageshow', () => manifestAppSetActive(true));
346
+ window.addEventListener('pagehide', () => manifestAppSetActive(false));
347
+ }
348
+
349
+ window.Alpine.magic('app', () => ({
350
+ get active() { const s = window.Alpine.store('app'); return s ? s.active : true; },
351
+ onChange(fn) { if (typeof fn === 'function') _manifestAppChangeHandlers.push(fn); }
352
+ }));
353
+ }
354
+
355
+
356
+ // $haptics — tactile feedback. Native: Capacitor Haptics. Web fallback:
357
+ // navigator.vibrate (limited; iOS Safari ignores it — fine, it's an enhancement).
358
+ // No-ops silently when unsupported.
359
+
360
+ function manifestVibrate(pattern) {
361
+ try { if (navigator.vibrate) return navigator.vibrate(pattern); } catch (e) {}
362
+ return false;
363
+ }
364
+
365
+ function initManifestHaptics() {
366
+ window.Alpine.magic('haptics', () => ({
367
+ impact(style) {
368
+ const H = manifestNativePlugin('Haptics');
369
+ if (H) { try { return H.impact({ style: style || 'MEDIUM' }); } catch (e) {} }
370
+ manifestVibrate(10); return Promise.resolve();
371
+ },
372
+ notification(type) {
373
+ const H = manifestNativePlugin('Haptics');
374
+ if (H) { try { return H.notification({ type: type || 'SUCCESS' }); } catch (e) {} }
375
+ manifestVibrate([10, 40, 10]); return Promise.resolve();
376
+ },
377
+ selection() {
378
+ const H = manifestNativePlugin('Haptics');
379
+ if (H) { try { return H.selectionChanged(); } catch (e) {} }
380
+ manifestVibrate(5); return Promise.resolve();
381
+ },
382
+ vibrate(ms) {
383
+ const H = manifestNativePlugin('Haptics');
384
+ if (H) { try { return H.vibrate({ duration: ms || 300 }); } catch (e) {} }
385
+ manifestVibrate(ms || 300); return Promise.resolve();
386
+ }
387
+ }));
388
+ }
389
+
390
+
391
+ // $biometric — Face ID / Touch ID. Native: a Capacitor biometric plugin
392
+ // (BiometricAuth or NativeBiometric; method shapes differ, adapted below). The web
393
+ // has no equivalent one-shot verify (WebAuthn is a separate server ceremony), so
394
+ // available() is false and verify() reports unsupported — a genuine native win.
395
+
396
+ function manifestBiometricPlugin() {
397
+ return manifestNativePlugin('BiometricAuth') || manifestNativePlugin('NativeBiometric') || null;
398
+ }
399
+
400
+ async function manifestBiometricAvailable() {
401
+ const B = manifestBiometricPlugin();
402
+ if (!B) return false;
403
+ try {
404
+ if (typeof B.checkBiometry === 'function') { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
405
+ if (typeof B.isAvailable === 'function') { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
406
+ } catch (e) {}
407
+ return false;
408
+ }
409
+
410
+ async function manifestBiometricVerify(opts) {
411
+ const o = opts || {};
412
+ const B = manifestBiometricPlugin();
413
+ if (!B) return { verified: false, error: 'unsupported' };
414
+ try {
415
+ if (typeof B.authenticate === 'function') { await B.authenticate({ reason: o.reason, title: o.title, subtitle: o.subtitle, cancelTitle: o.cancelTitle }); return { verified: true }; }
416
+ if (typeof B.verifyIdentity === 'function') { await B.verifyIdentity({ reason: o.reason, title: o.title, subtitle: o.subtitle }); return { verified: true }; }
417
+ } catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed' }; }
418
+ return { verified: false, error: 'unsupported' };
419
+ }
420
+
421
+ function initManifestBiometric() {
422
+ window.Alpine.magic('biometric', () => ({
423
+ available: () => manifestBiometricAvailable(),
424
+ verify: (opts) => manifestBiometricVerify(opts)
425
+ }));
426
+ }
427
+
428
+
429
+ // $camera — capture or pick a photo. Native: Capacitor Camera.getPhoto. Web
430
+ // fallback: a file input (with capture hint on mobile) returning a data URL.
431
+ // Resolves { dataUrl?, format?, cancelled?, error? }.
432
+
433
+ function manifestCameraWeb(opts) {
434
+ const o = opts || {};
435
+ return new Promise(resolve => {
436
+ try {
437
+ const input = document.createElement('input');
438
+ input.type = 'file';
439
+ input.accept = 'image/*';
440
+ if (o.source !== 'photos') input.setAttribute('capture', 'environment');
441
+ input.style.position = 'fixed';
442
+ input.style.left = '-9999px';
443
+ let settled = false;
444
+ const done = (result) => {
445
+ if (settled) return;
446
+ settled = true;
447
+ window.removeEventListener('focus', onFocus);
448
+ input.remove();
449
+ resolve(result);
450
+ };
451
+ // No 'change' fires when the picker is cancelled; detect it on focus return.
452
+ const onFocus = () => setTimeout(() => { if (!settled && (!input.files || !input.files.length)) done({ cancelled: true }); }, 500);
453
+ input.addEventListener('change', () => {
454
+ const file = input.files && input.files[0];
455
+ if (!file) { done({ cancelled: true }); return; }
456
+ const reader = new FileReader();
457
+ reader.onload = () => done({ dataUrl: reader.result, format: (file.type.split('/')[1]) || '' });
458
+ reader.onerror = () => done({ cancelled: true, error: 'read-failed' });
459
+ reader.readAsDataURL(file);
460
+ });
461
+ window.addEventListener('focus', onFocus);
462
+ document.body.appendChild(input);
463
+ input.click();
464
+ } catch (e) { resolve({ cancelled: true, error: 'unsupported' }); }
465
+ });
466
+ }
467
+
468
+ function manifestCameraPhoto(opts) {
469
+ const o = opts || {};
470
+ const Cam = manifestNativePlugin('Camera');
471
+ if (Cam) {
472
+ return Cam.getPhoto({ resultType: 'dataUrl', source: o.source === 'photos' ? 'PHOTOS' : 'CAMERA', quality: o.quality || 90 })
473
+ .then(r => ({ dataUrl: r && (r.dataUrl || r.webPath), format: r && r.format }))
474
+ .catch(e => ({ cancelled: true, error: (e && (e.message || e.code)) || 'cancelled' }));
475
+ }
476
+ return manifestCameraWeb(o);
477
+ }
478
+
479
+ function initManifestCamera() {
480
+ window.Alpine.magic('camera', () => ({
481
+ photo: (opts) => manifestCameraPhoto(opts),
482
+ pick: (opts) => manifestCameraPhoto(Object.assign({}, opts || {}, { source: 'photos' }))
483
+ }));
484
+ }
@@ -269,6 +269,11 @@
269
269
  }
270
270
  },
271
271
  "additionalProperties": true
272
+ },
273
+ "native": {
274
+ "type": "object",
275
+ "description": "Opts the app into the native umbrella plugin — $-magics for biometric, push, haptics, share, secure storage, deep links, camera, and app lifecycle, plus $device enrichment. Auto-loaded inside a Capacitor container; declaring this block also loads it on the web build (every capability degrades gracefully off-device, with Capacitor as one adapter). An empty object is enough to opt in.",
276
+ "additionalProperties": true
272
277
  }
273
278
  },
274
279
  "definitions": {
@@ -475,4 +475,31 @@
475
475
  html[data-os="ios"] .no-ios { display: none !important }
476
476
  html[data-os="android"] .no-android { display: none !important }
477
477
  html[data-os="macos"] .no-apple, html[data-os="ios"] .no-apple { display: none !important }
478
+
479
+ /* Visibility — connectivity & container (html[data-*] set by the $device signal) */
480
+ html:not([data-online="false"]) .offline-only { display: none !important }
481
+ html[data-online="false"] .online-only { display: none !important }
482
+ html:not([data-standalone]) .standalone-only { display: none !important }
483
+ html:not([data-native]) .native-only { display: none !important }
484
+ html[data-native] .web-only { display: none !important }
485
+
486
+ /* Safe-area padding (var --safe-* is 0 unless viewport-fit=cover on a notched screen) */
487
+ :where(.pt-safe) { padding-top: var(--safe-top) }
488
+ :where(.pr-safe) { padding-right: var(--safe-right) }
489
+ :where(.pb-safe) { padding-bottom: var(--safe-bottom) }
490
+ :where(.pl-safe) { padding-left: var(--safe-left) }
491
+ :where(.px-safe) { padding-left: var(--safe-left); padding-right: var(--safe-right) }
492
+ :where(.py-safe) { padding-top: var(--safe-top); padding-bottom: var(--safe-bottom) }
493
+ :where(.p-safe) { padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left) }
494
+
495
+ /* Dynamic viewport height (avoids the mobile 100vh browser-chrome jump) */
496
+ :where(.min-h-dvh) { min-height: 100dvh }
497
+ :where(.h-dvh) { height: 100dvh }
498
+
499
+ /* Native — suppress web tells (opt-in) */
500
+ :where(.no-callout) { -webkit-touch-callout: none }
501
+ :where(.no-select) { -webkit-user-select: none; user-select: none }
502
+ :where(.no-tap-zoom) { touch-action: manipulation }
503
+ :where(.no-overscroll) { overscroll-behavior: none }
504
+ :where(.no-overscroll-y) { overscroll-behavior-y: none }
478
505
  }
@@ -3476,6 +3476,78 @@ TailwindCompiler.prototype.startProcessing = async function () {
3476
3476
 
3477
3477
 
3478
3478
 
3479
+ // Device signal — $device (os / touch / online / standalone / native / platform).
3480
+ // Stamps html[data-online|standalone|native] for CSS variants; $device mirrors it reactively.
3481
+
3482
+ // Stamp device-state attributes before first paint. Honors values already set
3483
+ // (prerenderer, the native umbrella, or manual); data-os is set by detectOS.
3484
+ function stampManifestDeviceAttrs() {
3485
+ try {
3486
+ const html = document.documentElement;
3487
+ if (!html) return;
3488
+ const standalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true;
3489
+ if (standalone && !html.hasAttribute('data-standalone')) html.setAttribute('data-standalone', '');
3490
+ if (!html.hasAttribute('data-native')) {
3491
+ const cap = window.Capacitor;
3492
+ const native = !!(cap && (typeof cap.isNativePlatform !== 'function' || cap.isNativePlatform()));
3493
+ if (native) html.setAttribute('data-native', '');
3494
+ }
3495
+ html.setAttribute('data-online', navigator.onLine === false ? 'false' : 'true');
3496
+ } catch (e) {
3497
+ // Non-fatal: device-scoped utilities simply won't match.
3498
+ }
3499
+ }
3500
+ stampManifestDeviceAttrs();
3501
+
3502
+ // Keep html[data-online] and the reactive store in sync with connectivity changes.
3503
+ function syncManifestDeviceOnline() {
3504
+ try {
3505
+ const online = navigator.onLine !== false;
3506
+ document.documentElement.setAttribute('data-online', online ? 'true' : 'false');
3507
+ if (window.Alpine && typeof window.Alpine.store === 'function') {
3508
+ const store = window.Alpine.store('device');
3509
+ if (store) store.online = online;
3510
+ }
3511
+ } catch (e) {}
3512
+ }
3513
+ window.addEventListener('online', syncManifestDeviceOnline);
3514
+ window.addEventListener('offline', syncManifestDeviceOnline);
3515
+
3516
+ // Register $device + its reactive store once Alpine is available.
3517
+ let manifestDeviceInitialized = false;
3518
+ function initManifestDeviceSignal() {
3519
+ const html = document.documentElement;
3520
+ window.Alpine.store('device', { online: navigator.onLine !== false });
3521
+ window.Alpine.magic('device', () => ({
3522
+ get os() { return html.getAttribute('data-os') || ''; },
3523
+ get touch() { return (navigator.maxTouchPoints || 0) > 1 || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) || false; },
3524
+ get online() { const s = window.Alpine.store('device'); return s ? s.online !== false : navigator.onLine !== false; },
3525
+ get standalone() { return html.hasAttribute('data-standalone') || (!!window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true; },
3526
+ get native() { return html.hasAttribute('data-native'); },
3527
+ get platform() { return html.getAttribute('data-platform') || (html.hasAttribute('data-native') ? (html.getAttribute('data-os') || 'native') : 'web'); }
3528
+ }));
3529
+ }
3530
+ function ensureManifestDeviceInitialized() {
3531
+ if (manifestDeviceInitialized) return;
3532
+ if (!window.Alpine || typeof window.Alpine.magic !== 'function' || typeof window.Alpine.store !== 'function') return;
3533
+ manifestDeviceInitialized = true;
3534
+ initManifestDeviceSignal();
3535
+ }
3536
+ window.ensureManifestDeviceInitialized = ensureManifestDeviceInitialized;
3537
+ document.addEventListener('alpine:init', ensureManifestDeviceInitialized);
3538
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
3539
+ setTimeout(ensureManifestDeviceInitialized, 0);
3540
+ } else {
3541
+ const manifestDeviceCheck = setInterval(() => {
3542
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
3543
+ clearInterval(manifestDeviceCheck);
3544
+ ensureManifestDeviceInitialized();
3545
+ }
3546
+ }, 10);
3547
+ setTimeout(() => clearInterval(manifestDeviceCheck), 5000);
3548
+ }
3549
+
3550
+
3479
3551
  // Utilities initialization: create compiler, set up event listeners
3480
3552
 
3481
3553
  // Stamp <html data-os> so OS variants (mac:, ios:, …) resolve in pure CSS
@@ -3520,7 +3592,12 @@ function injectTailwindVariants() {
3520
3592
  '@custom-variant linux (&:where([data-os="linux"] *));',
3521
3593
  '@custom-variant ios (&:where([data-os="ios"] *));',
3522
3594
  '@custom-variant android (&:where([data-os="android"] *));',
3523
- '@custom-variant apple (&:where([data-os="macos"] *, [data-os="ios"] *));'
3595
+ '@custom-variant apple (&:where([data-os="macos"] *, [data-os="ios"] *));',
3596
+ '@custom-variant online (&:where([data-online="true"] *));',
3597
+ '@custom-variant offline (&:where([data-online="false"] *));',
3598
+ '@custom-variant standalone (&:where([data-standalone] *));',
3599
+ '@custom-variant native (&:where([data-native] *));',
3600
+ '@custom-variant web (&:where(html:not([data-native]) *));'
3524
3601
  ].join('\n');
3525
3602
  (document.head || document.documentElement).appendChild(style);
3526
3603
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.171",
3
+ "version": "0.5.172",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",