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.
@@ -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
- const response = await fetch(manifestUrl);
32
- const manifest = await response.json();
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
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.199-next.2';
5
+ const MANIFEST_BUILD_VERSION = '0.5.199-next.4';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -121,10 +121,20 @@
121
121
  state.resetQueued = true;
122
122
  queueMicrotask(() => { state.resetQueued = false; reset(); });
123
123
  };
124
- window.addEventListener('manifest:persist:scope', queue);
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)) if (k !== 'body' && k !== '_seq') target[k] = incoming[k];
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.persist !== false) detach = P().attach(conversationId, { snapshot, reset, stale: () => state.stale, count: () => _msgs.length });
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 native plugin when present). */
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 native plugin — native sheet, web share, or clipboard fallback. */
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 native plugin — push/notification permissions and token. */
285
+ /** `$push` from the device plugin — push/notification permissions and token. */
286
286
  export interface ManifestPush {
287
- request(): Promise<string>;
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 native plugin — secure key-value storage (native keychain or fallback). */
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 native plugin — deep/universal link handling. */
309
+ /** `$links` from the device plugin — deep/universal link handling. */
303
310
  export interface ManifestLinks {
304
- on(handler: (url: string) => void): void;
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 native plugin — foreground/background state. */
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 native plugin — haptic feedback with web vibration fallback. */
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 native plugin. */
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 native plugin. */
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 native plugin). */
478
+ /** Device/platform info (utilities; enriched by the device plugin). */
469
479
  const $device: ManifestDevice;
470
- /** Native share (native plugin). */
480
+ /** Native share (device plugin). */
471
481
  const $share: ManifestShare;
472
- /** Push permissions (native plugin). */
482
+ /** Push permissions (device plugin). */
473
483
  const $push: ManifestPush;
474
- /** Secure storage (native plugin). */
484
+ /** Secure storage (device plugin). */
475
485
  const $secure: ManifestSecure;
476
- /** Deep links (native plugin). */
486
+ /** Deep links (device plugin). */
477
487
  const $links: ManifestLinks;
478
- /** App foreground state (native plugin). */
488
+ /** App foreground state (device plugin). */
479
489
  const $app: ManifestApp;
480
- /** Haptics (native plugin). */
490
+ /** Haptics (device plugin). */
481
491
  const $haptics: ManifestHaptics;
482
- /** Biometric auth (native plugin). */
492
+ /** Biometric auth (device plugin). */
483
493
  const $biometric: ManifestBiometric;
484
- /** Camera (native plugin). */
494
+ /** Camera (device plugin). */
485
495
  const $camera: ManifestCamera;
486
496
 
487
497
  /** Window-level Manifest namespace exposed by the loader. */
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.199-next.2';
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
- const response = await fetch(manifestUrl);
27
- const manifest = await response.json();
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
 
@@ -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 = 8;
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
+ })();
@@ -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 }) => registerEdit(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-Dy5NIqlV3TdKC87VYMqikQ2kZAPGdGy7wVIqcR92XYb47v+0Dn/YTPZsJdg6YhMT",
3
- "manifest.appwrite.data.js": "sha384-rBxJHvm65pKByB3p5uNmCPvm56LXOJ5BFprVuZPcEFjSbi17WBQL0q49hAXl5gxW",
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-5SEd0T5cInCswUcbAH7CuKYuzCu9nPoGOsCjSo22UVksHkxZ5OY78ZWTjeVQtR9M",
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-HEyeJtV20k7po8WH0LEj800MlsqgDH0vE5GTAl62SgdEhn1aJ7VfryQcA6yIi18U",
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-2PDbtqCZnYLWR4enzYvj93csIQX2pC1HhFDmTGFrTb3v0IS2FzZ0cgatZ6RPR50y",
14
+ "manifest.data.js": "sha384-Cfww6fFCDaSckoFAUvLRf7ResiGMSmmLgNeZQLe3JbqHZufiwbhRMzTyw3FaOL0l",
15
15
  "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
16
- "manifest.defer.js": "sha384-t/g++osVS3qDXeY8+gfxUtLF9o0SCQQJKNxHGpb8NkIciDvxOAvA6sFIVcM0AKeA",
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-ZvgV3ax7yHtSC23zdvRYJPzBsBpGFBLuBJMgfQ1IDumv97Cn2bpHOePxyQikfl5L",
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-nIngj4CM5hV4bvQvgs8C7JmSy1UUzobsbhvDE2NH5dXqQVNW/Hiat34wHUxMqSaO",
24
- "manifest.payments.js": "sha384-QGctMZtxF0Wb2JUJbb9sJ4bFbyQvUdr9HaKs4afyaTXb7zedAAIWeB27XINMe3VZ",
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-fXqDnKwldB7XRxcfEQYTm/WqLl2oP24xY2Znq8FqnFM9LQo0zrQkXszqAOksArUX",
29
+ "manifest.status.js": "sha384-nwvUSbT+gpRMqhYx6MeBYswUMH72rY92mjgxv0vrSG7Ht7l83XYpEB6fCTbXjjfK",
29
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
30
- "manifest.sw.js": "sha384-lzc4R7OpLBlr8VS6OfudGQVrKdEny+1etglkfsLUQ3pX0Z3JJWagy0rl/amf5xnW",
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-1LsuOJyMCgab7W9w6DJEu5URzbadYxtq/cHtB8sN3hBFa/BHPnjdnTRbiKyM0FcC",
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-jicPS2+j2BtRtkdXC8YqGOQFi/V/E9MMjJFQ8mbDult9IG2cyzwq191W+zIl137b"
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
- 'native': ['utilities']
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.onload = () => { script.setAttribute('data-mnfst-loaded', ''); resolve(); };
410
- script.onerror = () => { script.remove(); reject(new Error(`Failed to load ${url}`)); };
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 native umbrella plugin. Auto-loaded inside a Capacitor container
544
- // (window.Capacitor present) or when a `native` config block opts in on the web.
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 ['native'];
547
- if (manifest && typeof manifest === 'object' && manifest.native && typeof manifest.native === 'object') return ['native'];
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', 'native'];
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 fetch(manifestUrl).then(r => r.ok ? r.json() : null).catch(() => null);
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 = fetch(manifestUrl).then(r => r.ok ? r.json() : null).catch(() => null);
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('native')) {
854
- pluginsToLoad = resolveDependencies([...pluginsToLoad, 'native']);
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
- // Resolve ${VAR} before any plugin reads the cached manifest —
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;
@@ -1,4 +1,4 @@
1
- /* manifest.native.js — built from scripts/native/ */
1
+ /* manifest.device.js — built from scripts/device/ */
2
2
 
3
3
  (function () {
4
4
 
@@ -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
- const res = await fetch(url);
27
- return await res.json();
27
+ window.__manifestPromise = fetch(url).then(r => r.json());
28
+ return await window.__manifestPromise;
28
29
  } catch (_) {
29
30
  return null;
30
31
  }
@@ -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": "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.",
418
+ "description": "Deprecated alias of `device` (renamed in 0.5.199); still accepted.",
414
419
  "additionalProperties": true
415
420
  }
416
421
  },
@@ -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 && cached.status) return 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
- const res = await fetch(url);
71
- return await res.json();
71
+ window.__manifestPromise = fetch(url).then(r => r.json());
72
+ return await window.__manifestPromise;
72
73
  } catch (_) {
73
- return cached || null;
74
+ return null;
74
75
  }
75
76
  }
76
77
 
@@ -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.2';
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) { saveRange(a); lastFocused = a; report(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
- run(el, id, id === 'a' ? undefined : undefined);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.199-next.2",
3
+ "version": "0.5.199-next.4",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",