mnfst 0.5.198-next.3 → 0.5.198

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,31 @@
1
+ /* Manifest Bindings */
2
+
3
+ (function () {
4
+
5
+ // x-text: same as Alpine's, but never rewrites an unchanged string (each write is a DOM mutation)
6
+ function initializeBindingsPlugin() {
7
+ Alpine.directive('text', (el, { expression }, { effect, evaluateLater }) => {
8
+ const evaluate = evaluateLater(expression);
9
+ effect(() => {
10
+ evaluate((value) => {
11
+ const next = value == null ? '' : String(value);
12
+ if (el.textContent === next) return;
13
+ Alpine.mutateDom(() => { el.textContent = next; });
14
+ });
15
+ });
16
+ });
17
+ }
18
+
19
+ let bindingsPluginInitialized = false;
20
+
21
+ function ensureBindingsPluginInitialized() {
22
+ if (bindingsPluginInitialized) return;
23
+ if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
24
+ bindingsPluginInitialized = true;
25
+ initializeBindingsPlugin();
26
+ }
27
+
28
+ document.addEventListener('alpine:init', ensureBindingsPluginInitialized);
29
+ if (window.Alpine && typeof window.Alpine.directive === 'function') ensureBindingsPluginInitialized();
30
+
31
+ })();
@@ -279,6 +279,9 @@ const pendingLocal = new Map(); // source -> Map<$id, { patch, removed }>
279
279
  let allCache = null;
280
280
  let allDirty = true;
281
281
 
282
+ // Per-source freshness: `$fresh` resolves on the first network-fresh landing of this page-load
283
+ const freshness = new Map(); // source -> { promise, resolve, done }
284
+
282
285
  // Deep seal so Alpine won't proxy (double-proxying causes recursion)
283
286
  function deepSeal(obj) {
284
287
  if (obj === null || typeof obj !== 'object') {
@@ -372,10 +375,48 @@ function mergeRowFields(target, fields, dataSourceName) {
372
375
  }
373
376
 
374
377
  function attachMethods(array, dataSourceName) {
375
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
376
- if (Array.isArray(array) && loadDataSource && window.ManifestDataProxies?.attachArrayMethods) {
377
- window.ManifestDataProxies.attachArrayMethods(array, dataSourceName, loadDataSource);
378
+ const main = window.ManifestDataMain;
379
+ const reload = main?.reloadDataSource || main?.loadDataSource;
380
+ if (Array.isArray(array) && reload && window.ManifestDataProxies?.attachArrayMethods) {
381
+ window.ManifestDataProxies.attachArrayMethods(array, dataSourceName, reload);
382
+ }
383
+ }
384
+
385
+ function sourceFreshness(dataSourceName) {
386
+ let f = freshness.get(dataSourceName);
387
+ if (!f) {
388
+ f = { done: false, resolve: null, promise: null };
389
+ f.promise = new Promise(resolve => { f.resolve = resolve; });
390
+ freshness.set(dataSourceName, f);
378
391
  }
392
+ return f;
393
+ }
394
+
395
+ function markFresh(dataSourceName) {
396
+ const f = sourceFreshness(dataSourceName);
397
+ if (!f.done) { f.done = true; f.resolve(); }
398
+ }
399
+
400
+ // State-only write (loading/error) — rows stay live, no version bump
401
+ function setSourceState(dataSourceName, patch) {
402
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
403
+ if (!store) return null;
404
+ const key = `_${dataSourceName}_state`;
405
+ const current = store[key] || { loading: false, error: null, ready: false, stale: true };
406
+ const next = { ...current, ...patch };
407
+ if (patch.error !== undefined) next.errorTime = patch.error ? (patch.errorTime || Date.now()) : null;
408
+ store[key] = next;
409
+ return next;
410
+ }
411
+
412
+ // Authoritative in-flight map: one promise per key, cleared on settle
413
+ function runDeduped(key, factory) {
414
+ if (loadingPromises.has(key)) return loadingPromises.get(key);
415
+ const promise = Promise.resolve().then(factory).finally(() => {
416
+ if (loadingPromises.get(key) === promise) loadingPromises.delete(key);
417
+ });
418
+ loadingPromises.set(key, promise);
419
+ return promise;
379
420
  }
380
421
 
381
422
  // Reactive source array, created on demand; raw map tracks the same identity
@@ -466,14 +507,18 @@ function writeSource(dataSourceName, data, options = {}) {
466
507
  rawDataStore.set(dataSourceName, data);
467
508
  }
468
509
 
469
- const currentState = store[`_${dataSourceName}_state`] || { loading: false, error: null, ready: false };
510
+ const currentState = store[`_${dataSourceName}_state`] || { loading: false, error: null, ready: false, stale: true };
511
+ // `fresh`: network-origin rows → clears `stale` for the rest of the page-load
512
+ const fresh = options.fresh === true && data !== null && data !== undefined;
470
513
  const newState = {
471
514
  loading: options.loading !== undefined ? options.loading : currentState.loading,
472
515
  error: options.error !== undefined ? options.error : currentState.error,
473
516
  ready: options.ready !== undefined ? options.ready : (data !== null && data !== undefined),
474
- errorTime: options.error !== undefined && options.error !== null ? Date.now() : (currentState.errorTime || null)
517
+ errorTime: options.error !== undefined && options.error !== null ? Date.now() : (currentState.errorTime || null),
518
+ stale: fresh ? false : currentState.stale !== false
475
519
  };
476
520
  store[`_${dataSourceName}_state`] = newState;
521
+ if (fresh) markFresh(dataSourceName);
477
522
  store._initialized = true;
478
523
  store._ready = true;
479
524
 
@@ -1386,6 +1431,10 @@ window.ManifestDataStore = {
1386
1431
  landRemove,
1387
1432
  flushLandings,
1388
1433
  noteLocalWrite,
1434
+ // Stale-first + dedupe (§11.1)
1435
+ setSourceState,
1436
+ runDeduped,
1437
+ sourceFreshness,
1389
1438
  touchSource,
1390
1439
  bumpAllVersions,
1391
1440
  mergeRowFields,
@@ -3231,6 +3280,25 @@ function attachArrayMethods(array, dataSourceName, reloadDataSource) {
3231
3280
  }
3232
3281
  });
3233
3282
 
3283
+ // $stale / $fresh: semantics in manifest.data.proxies.magic.state.js
3284
+ Object.defineProperty(array, '$stale', {
3285
+ enumerable: false,
3286
+ configurable: true,
3287
+ get: function () {
3288
+ const store = Alpine.store('data');
3289
+ const state = store?.[`_${dataSourceName}_state`];
3290
+ return !state || state.stale !== false;
3291
+ }
3292
+ });
3293
+
3294
+ Object.defineProperty(array, '$fresh', {
3295
+ enumerable: false,
3296
+ configurable: true,
3297
+ get: function () {
3298
+ return window.ManifestDataStore?.sourceFreshness?.(dataSourceName)?.promise || Promise.resolve();
3299
+ }
3300
+ });
3301
+
3234
3302
  // Attach $search method for client-side text filtering
3235
3303
  Object.defineProperty(array, '$search', {
3236
3304
  enumerable: false,
@@ -3902,7 +3970,10 @@ function createArrayProxyWithRoute(arrayTarget, dataSourceName = null, reloadDat
3902
3970
  return undefined;
3903
3971
  }
3904
3972
 
3905
- // Handle state properties ($loading, $error, $ready)
3973
+ // Handle state properties ($loading, $error, $ready, $stale, $fresh)
3974
+ if (key === '$stale' || key === '$fresh') {
3975
+ return window.ManifestDataProxiesMagic?.getStateProperty?.(key, dataSourceName);
3976
+ }
3906
3977
  if (key === '$loading' || key === '$error' || key === '$ready') {
3907
3978
  const store = Alpine.store('data');
3908
3979
  // Safely access dataSourceName - if it's not defined, use null
@@ -4089,7 +4160,7 @@ function createArrayProxyWithRoute(arrayTarget, dataSourceName = null, reloadDat
4089
4160
  return true;
4090
4161
  }
4091
4162
  // Check custom methods we've added
4092
- if (key === '$route' || key === '$loading' || key === '$error' || key === '$ready') {
4163
+ if (key === '$route' || key === '$loading' || key === '$error' || key === '$ready' || key === '$stale' || key === '$fresh') {
4093
4164
  return true;
4094
4165
  }
4095
4166
  // For other string keys, check if they exist on the array
@@ -6947,16 +7018,22 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
6947
7018
  queries || [],
6948
7019
  scope
6949
7020
  );
6950
- const result = await window.ManifestDataAppwrite.loadTableRows(
6951
- appwriteConfig.databaseId,
6952
- tableId,
6953
- appwriteQueries
6954
- );
6955
- // Network landing: query result replaces the source (coalesced, identity-preserving)
6956
- if (result !== undefined && window.ManifestDataStore?.landRows) {
6957
- await window.ManifestDataStore.landRows(dataSourceName, result, { mode: 'replace' });
6958
- }
6959
- return result;
7021
+ // Dedupe key: source + serialized queries (never across different queries)
7022
+ const key = `${dataSourceName}:$query:${JSON.stringify(appwriteQueries.map(q => String(q)))}`;
7023
+ const { runDeduped, landRows } = window.ManifestDataStore || {};
7024
+ const run = async () => {
7025
+ const result = await window.ManifestDataAppwrite.loadTableRows(
7026
+ appwriteConfig.databaseId,
7027
+ tableId,
7028
+ appwriteQueries
7029
+ );
7030
+ // Network landing: query result replaces the source (coalesced, identity-preserving)
7031
+ if (result !== undefined && landRows) {
7032
+ await landRows(dataSourceName, result, { mode: 'replace', fresh: true });
7033
+ }
7034
+ return result;
7035
+ };
7036
+ return runDeduped ? await runDeduped(key, run) : await run();
6960
7037
  }
6961
7038
  }
6962
7039
 
@@ -7737,20 +7814,39 @@ window.ManifestDataProxiesAppwrite.createAppwriteMethodsHandler = createAppwrite
7737
7814
 
7738
7815
 
7739
7816
  /* Manifest Data Sources - Magic Method State Properties */
7740
- // $loading, $error, $ready state properties
7817
+ // $loading, $error, $ready, $stale, $fresh state properties
7818
+ //
7819
+ // $stale: true until the first network-fresh landing of this page-load is
7820
+ // applied (rows from a fetch, `$query`, or the memory cache of a fetch made
7821
+ // this page-load); false from then on — a later reload keeps it false and
7822
+ // reports through $loading. Single-reveal UIs gate on `!$stale`.
7823
+ // $fresh: promise resolving at that same first fresh landing; never rejects
7824
+ // (a failed reload keeps old rows and sets $error). Per page-load.
7825
+
7826
+ const STATE_PROPS = ['$loading', '$error', '$ready', '$stale', '$fresh'];
7827
+
7828
+ function defaultStateValue(prop) {
7829
+ if (prop === '$error') return null;
7830
+ if (prop === '$stale') return true;
7831
+ return false;
7832
+ }
7741
7833
 
7742
7834
  function getStateProperty(prop, dataSourceName) {
7835
+ if (prop === '$fresh') {
7836
+ return window.ManifestDataStore?.sourceFreshness?.(dataSourceName)?.promise || Promise.resolve();
7837
+ }
7838
+
7743
7839
  if (typeof Alpine === 'undefined' || !Alpine.store) {
7744
- return prop === '$loading' ? false : (prop === '$error' ? null : false);
7840
+ return defaultStateValue(prop);
7745
7841
  }
7746
7842
 
7747
7843
  const store = Alpine.store('data');
7748
7844
  if (!store) {
7749
- return prop === '$loading' ? false : (prop === '$error' ? null : false);
7845
+ return defaultStateValue(prop);
7750
7846
  }
7751
7847
 
7752
7848
  const stateKey = `_${dataSourceName}_state`;
7753
- const state = store[stateKey] || { loading: false, error: null, ready: false };
7849
+ const state = store[stateKey] || { loading: false, error: null, ready: false, stale: true };
7754
7850
 
7755
7851
  if (prop === '$loading') {
7756
7852
  return state.loading !== false; // Default to true if loading
@@ -7758,6 +7854,8 @@ function getStateProperty(prop, dataSourceName) {
7758
7854
  return state.error || null;
7759
7855
  } else if (prop === '$ready') {
7760
7856
  return state.ready || false;
7857
+ } else if (prop === '$stale') {
7858
+ return state.stale !== false;
7761
7859
  }
7762
7860
 
7763
7861
  return undefined;
@@ -7766,7 +7864,7 @@ function getStateProperty(prop, dataSourceName) {
7766
7864
  // State property handler for use in proxy get() traps
7767
7865
  function createStatePropertyHandler(dataSourceName) {
7768
7866
  return function (key) {
7769
- if (key === '$loading' || key === '$error' || key === '$ready') {
7867
+ if (STATE_PROPS.includes(key)) {
7770
7868
  return getStateProperty(key, dataSourceName);
7771
7869
  }
7772
7870
  return undefined;
@@ -7777,11 +7875,11 @@ function createStatePropertyHandler(dataSourceName) {
7777
7875
  if (!window.ManifestDataProxiesMagic) {
7778
7876
  window.ManifestDataProxiesMagic = {};
7779
7877
  }
7878
+ window.ManifestDataProxiesMagic.STATE_PROPS = STATE_PROPS;
7780
7879
  window.ManifestDataProxiesMagic.getStateProperty = getStateProperty;
7781
7880
  window.ManifestDataProxiesMagic.createStatePropertyHandler = createStatePropertyHandler;
7782
7881
 
7783
7882
 
7784
-
7785
7883
  /* Manifest Data Sources - Magic Method $files Handler */
7786
7884
  // Handles $files method for reactive file arrays
7787
7885
 
@@ -8250,10 +8348,14 @@ function registerXMagicMethod(loadDataSource) {
8250
8348
  window._manifestXAccessed = true;
8251
8349
  }
8252
8350
  const pendingLoads = new Map();
8351
+ const preDataProxies = new Map();
8253
8352
  const store = Alpine.store('data');
8254
8353
 
8255
- // Store loadDataSource in closure for Appwrite methods
8256
- const reloadDataSource = loadDataSource;
8354
+ // Reload variant for Appwrite methods: bypasses the memory cache, keeps rows live
8355
+ const reloadDataSource = (name, locale) => {
8356
+ const reload = window.ManifestDataMain?.reloadDataSource;
8357
+ return reload ? reload(name, locale) : loadDataSource(name, locale, { reload: true });
8358
+ };
8257
8359
 
8258
8360
  const ACTIVE_PROPS = Symbol('activeProps');
8259
8361
 
@@ -8285,7 +8387,8 @@ function registerXMagicMethod(loadDataSource) {
8285
8387
  const stamped = Array.isArray(data)
8286
8388
  ? data.map(d => (d && typeof d === 'object' && !('contentType' in d)) ? { contentType: name, ...d } : d)
8287
8389
  : data;
8288
- window.ManifestDataStore?.updateStore?.(name, stamped, { loading: false, error: null, ready: true, allowDuringInit: true });
8390
+ // App-supplied rows are authoritative → fresh
8391
+ window.ManifestDataStore?.updateStore?.(name, stamped, { loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
8289
8392
  return true;
8290
8393
  };
8291
8394
  }
@@ -8353,7 +8456,21 @@ function registerXMagicMethod(loadDataSource) {
8353
8456
  track();
8354
8457
  const createLoadingProxy = window.ManifestDataProxiesCore?.createLoadingProxy;
8355
8458
  if (createLoadingProxy) {
8356
- return createLoadingProxy(prop);
8459
+ // Cached per source so re-evaluations see one identity; state props
8460
+ // ($loading/$error/$ready/$stale/$fresh) answer before any rows exist
8461
+ if (!preDataProxies.has(prop)) {
8462
+ const loading = createLoadingProxy(prop);
8463
+ const stateProps = window.ManifestDataProxiesMagic?.STATE_PROPS || [];
8464
+ preDataProxies.set(prop, new Proxy(loading, {
8465
+ get(target, key) {
8466
+ if (stateProps.includes(key)) {
8467
+ return window.ManifestDataProxiesMagic.getStateProperty(key, prop);
8468
+ }
8469
+ return target[key];
8470
+ }
8471
+ }));
8472
+ }
8473
+ return preDataProxies.get(prop);
8357
8474
  }
8358
8475
  return window.ManifestDataProxiesCore?.getChainingFallback?.() ?? '';
8359
8476
  }
@@ -8461,7 +8578,7 @@ function registerXMagicMethod(loadDataSource) {
8461
8578
  const arrayForMethods = valueIsArray ? value : (rawIsArray ? rawValue : arrayToProxy);
8462
8579
 
8463
8580
  if (attachArrayMethods) {
8464
- const arrayWithMethods = attachArrayMethods(arrayForMethods, prop, loadDataSource);
8581
+ const arrayWithMethods = attachArrayMethods(arrayForMethods, prop, reloadDataSource);
8465
8582
  // has() trap so Alpine sees $search/$query/etc. before accessing
8466
8583
  return new Proxy(arrayWithMethods, {
8467
8584
  get(target, key) {
@@ -8479,7 +8596,7 @@ function registerXMagicMethod(loadDataSource) {
8479
8596
  const manifest = window.ManifestComponentsRegistry?.manifest || null;
8480
8597
  const dataSource = manifest?.data?.[prop] || manifest?.appwrite?.[prop];
8481
8598
  if (dataSource && window.ManifestDataConfig?.isAppwriteCollection?.(dataSource)) {
8482
- const methodsHandler = createAppwriteMethodsHandler(prop, loadDataSource);
8599
+ const methodsHandler = createAppwriteMethodsHandler(prop, reloadDataSource);
8483
8600
  return methodsHandler.bind(null, key);
8484
8601
  }
8485
8602
  } catch { /* fall through */ }
@@ -8497,7 +8614,8 @@ function registerXMagicMethod(loadDataSource) {
8497
8614
  has(target, key) {
8498
8615
  // Report that base plugin methods exist
8499
8616
  if (key === '$search' || key === '$query' || key === '$route' ||
8500
- key === '$loading' || key === '$error' || key === '$ready') {
8617
+ key === '$loading' || key === '$error' || key === '$ready' ||
8618
+ key === '$stale' || key === '$fresh') {
8501
8619
  return key in target;
8502
8620
  }
8503
8621
  // Report that array methods exist
@@ -10751,37 +10869,56 @@ async function checkFileMatchesScope(file, scope) {
10751
10869
  });
10752
10870
  }
10753
10871
 
10754
- // Load dataSource data
10755
- async function loadDataSource(dataSourceName, locale = 'en') {
10872
+ // Rows already in the store (array or object) — a reload keeps them live
10873
+ function hasLiveRows(dataSourceName) {
10874
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
10875
+ const raw = store ? (window.ManifestDataStore?.rawOf?.(store) || store) : null;
10876
+ const current = raw ? raw[dataSourceName] : undefined;
10877
+ return current !== null && current !== undefined;
10878
+ }
10879
+
10880
+ function liveLocale() {
10881
+ return (typeof document !== 'undefined' && document.documentElement?.lang)
10882
+ || (typeof Alpine !== 'undefined' && Alpine.store('locale')?.current)
10883
+ || 'en';
10884
+ }
10885
+
10886
+ // Load dataSource data. options.reload: bypass the memory cache and refetch;
10887
+ // existing rows stay live ($loading only), fresh rows merge by $id.
10888
+ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
10889
+ locale = locale || 'en';
10756
10890
  const cacheKey = `${dataSourceName}:${locale}`;
10757
- const { dataSourceCache, loadingPromises, isInitializing, updateStore, landRows } = window.ManifestDataStore;
10891
+ const { dataSourceCache, loadingPromises, isInitializing, updateStore, landRows, setSourceState, runDeduped } = window.ManifestDataStore;
10758
10892
 
10759
- // Memory cache. Serving cached data is fine, but writing an old locale to the
10760
- // live store would clobber a concurrent locale switch — so guard the store
10761
- // write via the `_locale` stamp (unstamped/non-localized data writes freely).
10762
- if (dataSourceCache.has(cacheKey)) {
10893
+ // Memory cache (this page-load's fetches). Serving cached data is fine, but
10894
+ // writing an old locale to the live store would clobber a concurrent locale
10895
+ // switch — so guard the store write via the `_locale` stamp.
10896
+ if (!options.reload && dataSourceCache.has(cacheKey)) {
10763
10897
  const cachedData = dataSourceCache.get(cacheKey);
10764
- const liveLocale = (window.Alpine && Alpine.store('locale')?.current)
10898
+ const live = (window.Alpine && Alpine.store('locale')?.current)
10765
10899
  || document.documentElement.lang || locale;
10766
- const staleLocaleHit = locale !== liveLocale && !!(cachedData && cachedData._locale);
10900
+ const staleLocaleHit = locale !== live && !!(cachedData && cachedData._locale);
10767
10901
  if (!isInitializing && !staleLocaleHit) {
10768
10902
  // Landing (coalesced): resolves once the cached rows are visible
10769
- await landRows(dataSourceName, cachedData, { mode: 'replace', loading: false, error: null, ready: true });
10903
+ await landRows(dataSourceName, cachedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true });
10770
10904
  }
10771
10905
  return cachedData;
10772
10906
  }
10773
10907
 
10774
- // If already loading, return existing promise
10908
+ // In flight for this source+locale: share it (every entry path lands here)
10775
10909
  if (loadingPromises.has(cacheKey)) {
10776
10910
  return loadingPromises.get(cacheKey);
10777
10911
  }
10778
10912
 
10779
- // Set loading state
10913
+ // Loading state: rows already landed stay live; first-ever load clears
10914
+ const keepRows = hasLiveRows(dataSourceName);
10780
10915
  if (!isInitializing) {
10781
- updateStore(dataSourceName, null, { loading: true, error: null, ready: false });
10916
+ if (keepRows) setSourceState(dataSourceName, { loading: true, error: null });
10917
+ else updateStore(dataSourceName, null, { loading: true, error: null, ready: false });
10782
10918
  }
10783
10919
 
10784
- const loadPromise = (async () => {
10920
+ return runDeduped(cacheKey, async () => {
10921
+ let landed = false;
10785
10922
  try {
10786
10923
  const manifest = await window.ManifestDataConfig.ensureManifest();
10787
10924
  if (!manifest) {
@@ -11043,23 +11180,29 @@ async function loadDataSource(dataSourceName, locale = 'en') {
11043
11180
  || document.documentElement.lang || locale;
11044
11181
  const staleLocale = localeSensitive && liveLocale !== locale;
11045
11182
 
11046
- // Network landing (coalesced per frame); resolves once visible
11183
+ // Network landing (coalesced per frame, merges by $id); resolves once visible
11047
11184
  if (!isInitializing && !staleLocale) {
11048
- await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true });
11185
+ landed = true;
11186
+ await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true });
11049
11187
  }
11050
11188
 
11051
11189
  // Return unsealed version for our proxy system
11052
11190
  return enhancedData;
11053
11191
  } catch (error) {
11054
- // Set error state with timestamp to prevent rapid retries
11192
+ // Error state (timestamped to prevent rapid retries); live rows stay
11055
11193
  if (!isInitializing) {
11194
+ landed = true;
11056
11195
  const errorMessage = error?.message || error?.toString() || `Failed to load dataSource "${dataSourceName}"`;
11057
- updateStore(dataSourceName, null, {
11058
- loading: false,
11059
- error: errorMessage,
11060
- ready: false,
11061
- errorTime: Date.now() // Track when error occurred
11062
- });
11196
+ if (hasLiveRows(dataSourceName)) {
11197
+ setSourceState(dataSourceName, { loading: false, error: errorMessage, errorTime: Date.now() });
11198
+ } else {
11199
+ updateStore(dataSourceName, null, {
11200
+ loading: false,
11201
+ error: errorMessage,
11202
+ ready: false,
11203
+ errorTime: Date.now() // Track when error occurred
11204
+ });
11205
+ }
11063
11206
  }
11064
11207
 
11065
11208
  // Only log non-auth errors to reduce noise (401 is expected until user authenticates)
@@ -11073,12 +11216,15 @@ async function loadDataSource(dataSourceName, locale = 'en') {
11073
11216
  }
11074
11217
  return null;
11075
11218
  } finally {
11076
- loadingPromises.delete(cacheKey);
11219
+ // Early exits (no source, stale locale) must not strand a live source in $loading
11220
+ if (!landed && keepRows && !isInitializing) setSourceState(dataSourceName, { loading: false });
11077
11221
  }
11078
- })();
11222
+ });
11223
+ }
11079
11224
 
11080
- loadingPromises.set(cacheKey, loadPromise);
11081
- return loadPromise;
11225
+ // Reload: refetch past the memory cache; rows stay live and merge by $id
11226
+ function reloadDataSource(dataSourceName, locale) {
11227
+ return loadDataSource(dataSourceName, locale || liveLocale(), { reload: true });
11082
11228
  }
11083
11229
 
11084
11230
  // Listen for URL changes to trigger reactivity
@@ -11203,7 +11349,8 @@ async function initializeDataSourcesPlugin() {
11203
11349
  // Export loadDataSource for use by team change listener
11204
11350
  window.ManifestDataMain = {
11205
11351
  loadDataSource,
11206
- _loadDataSource: loadDataSource, // Export for internal use
11352
+ reloadDataSource,
11353
+ _loadDataSource: reloadDataSource, // Internal: every user is a reload-after-mutation
11207
11354
  filterFilesByScope // Export for use by getFilesForEntry
11208
11355
  };
11209
11356
 
@@ -11216,18 +11363,16 @@ async function initializeDataSourcesPlugin() {
11216
11363
  const locale = document.documentElement.lang ||
11217
11364
  (typeof Alpine !== 'undefined' && Alpine.store('locale')?.current) || 'en';
11218
11365
 
11219
- const { dataSourceCache, loadingPromises } = window.ManifestDataStore;
11366
+ const { dataSourceCache } = window.ManifestDataStore;
11220
11367
  const isAppwriteCollection = window.ManifestDataConfig.isAppwriteCollection;
11221
11368
 
11222
11369
  for (const [name, source] of Object.entries(manifest.data)) {
11223
11370
  if (isAppwriteCollection(source)) continue;
11224
11371
  if (source && typeof source === 'object' && source.url) continue;
11225
11372
 
11226
- const cacheKey = `${name}:${locale}`;
11227
- dataSourceCache.delete(cacheKey);
11228
- loadingPromises.delete(cacheKey);
11229
-
11230
- try { await loadDataSource(name, locale); } catch { /* skip failed sources */ }
11373
+ dataSourceCache.delete(`${name}:${locale}`);
11374
+ // Reload keeps rows live and merges (identity survives an edit-save)
11375
+ try { await loadDataSource(name, locale, { reload: true }); } catch { /* skip failed sources */ }
11231
11376
  }
11232
11377
  });
11233
11378
 
@@ -11265,7 +11410,7 @@ async function initializeDataSourcesPlugin() {
11265
11410
  const data = await loadDataSource(name, locale);
11266
11411
  if (data != null) {
11267
11412
  // Boot landing: every pre-loaded source lands in ONE flush
11268
- await landRows(name, data, { mode: 'replace', loading: false, error: null, ready: true, allowDuringInit: true });
11413
+ await landRows(name, data, { mode: 'replace', loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
11269
11414
  }
11270
11415
  } catch (err) {
11271
11416
  console.warn(`[Manifest Data] Failed to pre-load ${name}:`, err);
@@ -11283,7 +11428,7 @@ async function initializeDataSourcesPlugin() {
11283
11428
  // allowDuringInit: setIsInitializing(true) is active, so without this
11284
11429
  // flag updateStore short-circuits and $x.manifest stays unpopulated.
11285
11430
  window.ManifestDataStore.dataSourceCache.set(`manifest:${locale}`, publicManifest);
11286
- updateStore('manifest', publicManifest, { loading: false, error: null, ready: true, allowDuringInit: true });
11431
+ updateStore('manifest', publicManifest, { loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
11287
11432
  }
11288
11433
  Alpine.store('data')._ready = true;
11289
11434
  } catch (error) {
@@ -12,7 +12,17 @@ const OWNS_CHILDREN = new Set([
12
12
  // Kill switch: data-defer="off" on the loader script; prerender snapshots keep eager markup
13
13
  const killed = !!document.querySelector('script[data-defer="off"]') || window.__manifestRender === true;
14
14
 
15
+ // Route deferral (spike, opt-in): data-defer-routes on the loader script or ManifestDeferConfig.routes
16
+ let routesScriptFlag = null;
17
+ function routesOn() {
18
+ const cfg = window.ManifestDeferConfig;
19
+ if (cfg && cfg.routes !== undefined) return cfg.routes === true;
20
+ if (routesScriptFlag === null) routesScriptFlag = !!document.querySelector('script[data-defer-routes]');
21
+ return routesScriptFlag;
22
+ }
23
+
15
24
  const pending = new Set();
25
+ const routes = new Set(); // deferred route panes: render on activation only, never prewarmed
16
26
  let ready = false;
17
27
  let bootDrained = false;
18
28
  let idleHandle = null;
@@ -73,12 +83,21 @@ function firstPanel(el) {
73
83
  return scope.querySelector(`[x-tabpanel="${set}"]`);
74
84
  }
75
85
 
86
+ // A route pane is closed when the router says it does not match the URL (or it is already hidden)
87
+ function routeInactive(el) {
88
+ if (el.hasAttribute('hidden')) return true;
89
+ const vis = window.ManifestRoutingVisibility;
90
+ return !!(vis && typeof vis.isRouteActive === 'function' && !vis.isRouteActive(el));
91
+ }
92
+
76
93
  // Closed-container rule, or the visibility rule for explicit x-defer
77
94
  function ruleFor(el, explicit) {
95
+ const isRoute = el.hasAttribute('x-route');
96
+ if (isRoute && routesOn() && routeInactive(el)) return 'route';
78
97
  if (el.hasAttribute('popover')) return isPopoverOpen(el) ? null : 'popover';
79
98
  const tag = el.tagName;
80
99
  if (tag === 'DIALOG' || tag === 'DETAILS') return el.hasAttribute('open') ? null : 'toggle';
81
- if (el.hasAttribute('hidden') && !el.hasAttribute('x-route')) return 'hidden';
100
+ if (el.hasAttribute('hidden') && !isRoute) return 'hidden';
82
101
  if (el.hasAttribute('x-tabpanel') && window.ensureTabsPluginInitialized) return firstPanel(el) === el ? null : 'panel';
83
102
  return explicit ? 'visible' : null;
84
103
  }
@@ -115,7 +134,8 @@ function register(el, rule, opts) {
115
134
  cleanup: []
116
135
  };
117
136
  wire(rec);
118
- if (!rec.lazy) { pending.add(rec); schedule(); }
137
+ if (rule === 'route') routes.add(rec);
138
+ else if (!rec.lazy) { pending.add(rec); schedule(); }
119
139
  return rec;
120
140
  }
121
141
 
@@ -208,6 +228,15 @@ function wire(rec) {
208
228
  });
209
229
  arm();
210
230
  if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
231
+ } else if (rule === 'route') {
232
+ // Router fires manifest:route-activate before it unhides the pane; hidden removal is the fallback
233
+ listen('manifest:route-activate', () => render(rec));
234
+ const arm = () => Alpine.onAttributeRemoved(el, 'hidden', () => {
235
+ if (!el.hasAttribute('hidden')) render(rec);
236
+ if (!rec.rendered || rec.discard) arm();
237
+ });
238
+ arm();
239
+ if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
211
240
  } else if (rule === 'panel') {
212
241
  observe(['style', 'x-show', 'x-tabpanel'], () => onState(shown(el, false)));
213
242
  } else if (rule === 'visible') {
@@ -218,11 +247,12 @@ function wire(rec) {
218
247
 
219
248
  // ---- Idle prewarm (after manifest:ready; urgent first, then priority asc, then document order) ----
220
249
 
221
- const idle = (fn, timeout) => {
250
+ // Genuine idle only: no timeout, so a never-idle page never prewarms (its stashed containers cost nothing)
251
+ const idle = (fn) => {
222
252
  idleIsTimer = !window.requestIdleCallback;
223
253
  return idleIsTimer
224
- ? setTimeout(() => fn({ didTimeout: true, timeRemaining: () => 0 }), 50)
225
- : window.requestIdleCallback(fn, { timeout });
254
+ ? setTimeout(() => fn({ didTimeout: false, timeRemaining: () => IDLE_BUDGET_MS + 1 }), 200)
255
+ : window.requestIdleCallback(fn);
226
256
  };
227
257
  function cancelIdle() {
228
258
  if (idleHandle === null) return;
@@ -232,7 +262,6 @@ function cancelIdle() {
232
262
 
233
263
  // A gesture that revealed a pane (tab switch, panel open) makes its containers urgent
234
264
  function promoteNearViewport() {
235
- promoteTimer = null;
236
265
  if (!ready || !bootDrained) return;
237
266
  // The on-screen containers nearest the gesture point (a revealed pane sits under the click)
238
267
  const candidates = [];
@@ -255,9 +284,11 @@ function promoteNearViewport() {
255
284
  function onGesture(e) {
256
285
  lastGestureAt = now();
257
286
  if (e && typeof e.clientX === 'number' && (e.clientX || e.clientY)) { gestureX = e.clientX; gestureY = e.clientY; }
258
- if (promoteTimer !== null || now() - lastPromoteAt < 500) return;
287
+ if (now() - lastPromoteAt < 300) return;
288
+ clearTimeout(promoteTimer);
259
289
  lastPromoteAt = now();
260
- promoteTimer = setTimeout(promoteNearViewport, 150);
290
+ // Three passes: a revealed pane lays out and mounts its menus over the next few hundred ms
291
+ promoteTimer = setTimeout(() => { promoteNearViewport(); promoteTimer = setTimeout(() => { promoteNearViewport(); promoteTimer = setTimeout(promoteNearViewport, 400); }, 150); }, 50);
261
292
  }
262
293
 
263
294
  // Only containers the user can reach: not under a hidden route; visible parents first
@@ -309,19 +340,20 @@ function schedule() {
309
340
  if (!head.urgent && warm.size >= PREWARM_CAP) return; // paused at the cap; resumes when a warm one opens or evicts
310
341
  idleHandle = idle((deadline) => {
311
342
  idleHandle = null;
312
- // Forced fire (never-idle page): one container. Genuine idle: batch within the budget.
343
+ // Render while the browser reports idle time; the first container of a slice always gets its turn
313
344
  let n = 0;
314
- do {
345
+ while (n < SLICE_MAX && (n === 0 || (deadline && deadline.timeRemaining() > IDLE_BUDGET_MS))) {
346
+ if (deadline && deadline.didTimeout) break;
315
347
  const rec = next();
316
348
  if (!rec) break;
317
349
  if (!rec.urgent && warm.size >= PREWARM_CAP) break; // cap = max warm; only urgent renders may exceed it
318
350
  for (const other of pending) other.near = undefined;
319
351
  render(rec, true);
320
352
  n++;
321
- } while (deadline && !deadline.didTimeout && deadline.timeRemaining() > IDLE_BUDGET_MS && n < SLICE_MAX);
353
+ }
322
354
  evict();
323
355
  schedule();
324
- }, head.urgent ? 100 : 500);
356
+ });
325
357
  }
326
358
 
327
359
  function markReady() {
@@ -370,7 +402,11 @@ window.ManifestDefer = {
370
402
  defer,
371
403
  render: (el) => { if (el && el.__mnfstDefer) render(el.__mnfstDefer); },
372
404
  isPending: (el) => !!(el && el.__mnfstDefer && !el.__mnfstDefer.rendered),
373
- stats: () => ({ pending: pending.size, warm: warm.size, cap: PREWARM_CAP, ready, bootDrained, armed: idleHandle !== null, head: (() => { const h = next(); return h ? (h.el.id || h.el.tagName) : null; })() })
405
+ stats: () => ({
406
+ pending: pending.size, warm: warm.size, cap: PREWARM_CAP, ready, bootDrained, armed: idleHandle !== null,
407
+ head: (() => { const h = next(); return h ? (h.el.id || h.el.tagName) : null; })(),
408
+ routes: { enabled: routesOn(), stashed: Array.from(routes).filter((r) => !r.rendered).length, rendered: Array.from(routes).filter((r) => r.rendered).length }
409
+ })
374
410
  };
375
411
  if (window.Manifest && typeof window.Manifest === 'object') window.Manifest.defer = defer;
376
412
 
@@ -2,6 +2,7 @@
2
2
  "manifest.appwrite.auth.js": "sha384-lWvffK6C2Zs9dOiMh/0OQTnU2ycZq1USOeJDlPUgJsbtrdA9+3zq4VHcIdS40qR0",
3
3
  "manifest.appwrite.data.js": "sha384-FusD+MQKkRQexKYl7j6egjDzsPYMWgtNublkgOl595aNqbx4yh8/PfbhLEyI06T1",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
+ "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
5
6
  "manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
6
7
  "manifest.chat.js": "sha384-UN5BmhqyafJ8OfLFaOLszrvUW5r001dRheQWqsEBCCLnfBcW1bjXaGOdZvURtrnL",
7
8
  "manifest.code.js": "sha384-aoQ4E26vrhNe/+tL7K0h90c8/0ies7e8oIR6Ay92he2X3dnWZ+OPcn2Dna8fmc+c",
@@ -10,9 +11,9 @@
10
11
  "manifest.combobox.js": "sha384-SN3XHumoebxcduFQqx5zt65/Cd10y8nuf9Gkco6UnsZ42sadRd+gvVLfWPdkkuPH",
11
12
  "manifest.components.js": "sha384-HEyeJtV20k7po8WH0LEj800MlsqgDH0vE5GTAl62SgdEhn1aJ7VfryQcA6yIi18U",
12
13
  "manifest.computed.js": "sha384-LDxER8pyYyYwUxIYIyiJA02s+49g8CzpSlOj28On0DFIUgetT3UmvE/9xOsTwT66",
13
- "manifest.data.js": "sha384-urUiANuQfrc+AfwTB7+nMneF2C1+EfV+S5+eg4PQnZCJ0Aid50KwSARufiVrGAXd",
14
+ "manifest.data.js": "sha384-LhX1a36AITeixPrC4RHz0X+TNMEoDwxi6JVclaIhNAlTYCtkYOHE2KmxJZhfJheD",
14
15
  "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
15
- "manifest.defer.js": "sha384-szMa04R4gWsZl3nICHz3Mtku8vgROEHvrAIMap+wuqITKvMVQHJOa0kVzHHjMcwN",
16
+ "manifest.defer.js": "sha384-t/g++osVS3qDXeY8+gfxUtLF9o0SCQQJKNxHGpb8NkIciDvxOAvA6sFIVcM0AKeA",
16
17
  "manifest.dropdowns.js": "sha384-4HuHkBt4i8sTPUDLB9rNLli2/lWLY1tjtCLzey37gRwueG+FRLCnVH1q7ljiVvJR",
17
18
  "manifest.edit.js": "sha384-ZvgV3ax7yHtSC23zdvRYJPzBsBpGFBLuBJMgfQ1IDumv97Cn2bpHOePxyQikfl5L",
18
19
  "manifest.export.js": "sha384-HCy3n6ifrcAGIyzTRgZ5JIRvSX9q9/8ykMKRSbt1QTnhXz6PgYlV0AE2N2Yi2re+",
@@ -22,7 +23,7 @@
22
23
  "manifest.native.js": "sha384-nIngj4CM5hV4bvQvgs8C7JmSy1UUzobsbhvDE2NH5dXqQVNW/Hiat34wHUxMqSaO",
23
24
  "manifest.payments.js": "sha384-QGctMZtxF0Wb2JUJbb9sJ4bFbyQvUdr9HaKs4afyaTXb7zedAAIWeB27XINMe3VZ",
24
25
  "manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
25
- "manifest.router.js": "sha384-uVGpY/d+v4fK0rR1wkePGe11LrNE1ePyZICv7L/ob9d1p/kGxu/3Z2D2ClFuQkr8",
26
+ "manifest.router.js": "sha384-4qmGEkRpgAo9xoQkW6+hHIhowvcyAggKB2J3Wf8tJtXp+L0hufoVAqzfhnqw4cNR",
26
27
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
27
28
  "manifest.status.js": "sha384-fXqDnKwldB7XRxcfEQYTm/WqLl2oP24xY2Znq8FqnFM9LQo0zrQkXszqAOksArUX",
28
29
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
@@ -34,5 +35,5 @@
34
35
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
35
36
  "manifest.utilities.js": "sha384-YcGKTEviTv9FLkw890CTociA7Rjp6xuhLyfpn3Gk4mT0fexDFqpW7VSuohcpGfsw",
36
37
  "manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
37
- "manifest.js": "sha384-IaRa1ZZNj8C/mcB9e6qNiTqK7oriQr0Vmt4Q7FcGWOqfUgPxojYixbs+g8XR9lNz"
38
+ "manifest.js": "sha384-4r0X5QSeiQ6bGMrw4Y8CDhTAS38w7CPXyxrid+l+sGWEwJ2XRG0STzrLU6XF+GIX"
38
39
  }
package/lib/manifest.js CHANGED
@@ -227,6 +227,7 @@
227
227
  'dropdowns',
228
228
  'combobox',
229
229
  'computed',
230
+ 'bindings',
230
231
  'tabs',
231
232
  'text-edit',
232
233
  'slides',
@@ -636,39 +636,21 @@ function isPrerenderedStaticMPA() {
636
636
  }
637
637
  }
638
638
 
639
- // Process visibility for all elements with x-route
640
- function processRouteVisibility(normalizedPath) {
641
- // Static prerender output already contains only this route's sections; x-cloak + toggling here
642
- // causes a visible flash (content → hidden via x-cloak → shown when Alpine boots).
643
- if (isPrerenderedStaticMPA()) return;
644
-
645
- const routeElements = document.querySelectorAll('[x-route]');
646
-
647
- // First pass: collect all defined routes (excluding !* and other negative conditions)
648
- const definedRoutes = [];
649
- routeElements.forEach(element => {
650
- const routeCondition = element.getAttribute('x-route');
651
- if (!routeCondition) return;
652
-
653
- const conditions = routeCondition.split(',').map(cond => cond.trim());
654
- conditions.forEach(cond => {
655
- // Only collect positive conditions and wildcards (not negative ones)
656
- if (!cond.startsWith('!') && cond !== '!*') {
657
- definedRoutes.push(cond);
658
- }
659
- });
660
- });
639
+ // Logical path for the current URL, normalized the way route conditions expect
640
+ function currentNormalizedPath() {
641
+ const currentPath = window.ManifestRoutingNavigation?.getCurrentRoute() ?? window.location.pathname;
642
+ return currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
643
+ }
661
644
 
662
- // Extract localization codes from manifest.json data sources
645
+ // Localization codes from manifest.json data sources
646
+ function getLocalizationCodes() {
663
647
  const localizationCodes = [];
664
648
  try {
665
- // Check if manifest is available and has data sources
666
649
  const manifest = window.ManifestComponentsRegistry?.manifest || window.manifest;
667
650
  if (manifest && manifest.data) {
668
651
  Object.values(manifest.data).forEach(dataSource => {
669
652
  if (typeof dataSource === 'object' && dataSource !== null) {
670
653
  Object.keys(dataSource).forEach(key => {
671
- // Check if this looks like a localization key (common language codes)
672
654
  if (key.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
673
655
  localizationCodes.push(key);
674
656
  }
@@ -679,80 +661,117 @@ function processRouteVisibility(normalizedPath) {
679
661
  } catch (e) {
680
662
  // Ignore errors if manifest is not available
681
663
  }
664
+ return localizationCodes;
665
+ }
682
666
 
683
- // Check if current route is defined by any route
684
- let isRouteDefined = definedRoutes.some(route =>
667
+ // Positive conditions of every route, including routes stashed inside a deferred route
668
+ function collectDefinedRoutes() {
669
+ const definedRoutes = [];
670
+ const collect = (element) => {
671
+ const routeCondition = element.getAttribute('x-route');
672
+ if (!routeCondition) return;
673
+ routeCondition.split(',').map(cond => cond.trim()).forEach(cond => {
674
+ if (!cond.startsWith('!') && cond !== '!*') definedRoutes.push(cond);
675
+ });
676
+ };
677
+ document.querySelectorAll('[x-route]').forEach(collect);
678
+ document.querySelectorAll('[x-route] > template[data-mnfst-defer]').forEach(tpl => {
679
+ tpl.content.querySelectorAll('[x-route]').forEach(collect);
680
+ });
681
+ return definedRoutes;
682
+ }
683
+
684
+ // Whether any defined route (or its localized form) covers the path — drives x-route="!*"
685
+ function isRouteDefined(normalizedPath, definedRoutes) {
686
+ let defined = definedRoutes.some(route =>
685
687
  window.ManifestRouting.matchesCondition(normalizedPath, route)
686
688
  );
687
-
688
- // Also check if the route starts with a localization code
689
- if (!isRouteDefined && localizationCodes.length > 0) {
689
+ const localizationCodes = getLocalizationCodes();
690
+ if (!defined && localizationCodes.length > 0) {
690
691
  const pathSegments = normalizedPath.split('/').filter(segment => segment);
691
- if (pathSegments.length > 0) {
692
- const firstSegment = pathSegments[0];
693
- if (localizationCodes.includes(firstSegment)) {
694
- // This is a localized route - check if the remaining path is defined
695
- const remainingPath = pathSegments.slice(1).join('/');
696
-
697
- // If no remaining path, treat as root route
698
- if (remainingPath === '') {
699
- isRouteDefined = definedRoutes.some(route =>
700
- window.ManifestRouting.matchesCondition('/', route) ||
701
- window.ManifestRouting.matchesCondition('', route)
702
- );
703
- } else {
704
- // Check if the remaining path matches any defined route
705
- isRouteDefined = definedRoutes.some(route =>
706
- window.ManifestRouting.matchesCondition(remainingPath, route)
707
- );
708
- }
692
+ if (pathSegments.length > 0 && localizationCodes.includes(pathSegments[0])) {
693
+ const remainingPath = pathSegments.slice(1).join('/');
694
+ if (remainingPath === '') {
695
+ defined = definedRoutes.some(route =>
696
+ window.ManifestRouting.matchesCondition('/', route) ||
697
+ window.ManifestRouting.matchesCondition('', route)
698
+ );
699
+ } else {
700
+ defined = definedRoutes.some(route =>
701
+ window.ManifestRouting.matchesCondition(remainingPath, route)
702
+ );
709
703
  }
710
704
  }
711
705
  }
706
+ return defined;
707
+ }
708
+
709
+ // Match one route element against a path: true/false, or null when it carries no condition
710
+ function routeMatches(element, normalizedPath, defined) {
711
+ const routeCondition = element.getAttribute('x-route');
712
+ if (!routeCondition) return null;
713
+
714
+ const conditions = routeCondition.split(',').map(cond => cond.trim());
715
+ if (conditions.includes('!*')) {
716
+ if (defined === undefined) defined = isRouteDefined(normalizedPath, collectDefinedRoutes());
717
+ return !defined;
718
+ }
712
719
 
713
- routeElements.forEach(element => {
714
- const routeCondition = element.getAttribute('x-route');
715
- if (!routeCondition) return;
720
+ const positiveConditions = conditions.filter(cond => !cond.startsWith('!'));
721
+ const negativeConditions = conditions
722
+ .filter(cond => cond.startsWith('!'))
723
+ .map(cond => cond.slice(1));
724
+ const hasNegativeMatch = negativeConditions.some(cond =>
725
+ window.ManifestRouting.matchesCondition(normalizedPath, cond)
726
+ );
727
+ const hasPositiveMatch = positiveConditions.length === 0 || positiveConditions.some(cond =>
728
+ window.ManifestRouting.matchesCondition(normalizedPath, cond)
729
+ );
730
+ return hasPositiveMatch && !hasNegativeMatch;
731
+ }
716
732
 
717
- // Parse route conditions
718
- const conditions = routeCondition.split(',').map(cond => cond.trim());
719
- const positiveConditions = conditions.filter(cond => !cond.startsWith('!'));
720
- const negativeConditions = conditions
721
- .filter(cond => cond.startsWith('!'))
722
- .map(cond => cond.slice(1));
723
-
724
- // Special handling for !* (undefined routes)
725
- if (conditions.includes('!*')) {
726
- const shouldShow = !isRouteDefined;
727
- if (shouldShow) {
728
- element.removeAttribute('hidden');
729
- element.style.display = '';
730
- } else {
731
- element.setAttribute('hidden', '');
732
- element.style.display = 'none';
733
- }
734
- return;
735
- }
733
+ // Cooperative check (defer plugin): is this route active for the current URL? Static MPA output always is.
734
+ function isRouteActive(element, normalizedPath) {
735
+ if (isPrerenderedStaticMPA()) return true;
736
+ return routeMatches(element, normalizedPath === undefined ? currentNormalizedPath() : normalizedPath) !== false;
737
+ }
736
738
 
737
- // Check conditions
738
- const hasNegativeMatch = negativeConditions.some(cond =>
739
- window.ManifestRouting.matchesCondition(normalizedPath, cond)
740
- );
741
- const hasPositiveMatch = positiveConditions.length === 0 || positiveConditions.some(cond =>
742
- window.ManifestRouting.matchesCondition(normalizedPath, cond)
743
- );
739
+ // Activation hook fires before the route becomes visible so deferred content renders first
740
+ function showRoute(element) {
741
+ element.dispatchEvent(new CustomEvent('manifest:route-activate'));
742
+ element.removeAttribute('hidden');
743
+ element.style.display = '';
744
+ }
744
745
 
745
- const shouldShow = hasPositiveMatch && !hasNegativeMatch;
746
+ function hideRoute(element) {
747
+ element.setAttribute('hidden', '');
748
+ element.style.display = 'none';
749
+ }
746
750
 
747
- // Show/hide element
748
- if (shouldShow) {
749
- element.removeAttribute('hidden');
750
- element.style.display = '';
751
+ // Process visibility for all elements with x-route
752
+ function processRouteVisibility(normalizedPath) {
753
+ // Static prerender output already contains only this route's sections; x-cloak + toggling here
754
+ // causes a visible flash (content → hidden via x-cloak → shown when Alpine boots).
755
+ if (isPrerenderedStaticMPA()) return;
756
+
757
+ const defined = isRouteDefined(normalizedPath, collectDefinedRoutes());
758
+
759
+ // Worklist: activating a deferred route can reveal nested routes that need this pass too
760
+ const seen = new Set();
761
+ const queue = Array.from(document.querySelectorAll('[x-route]'));
762
+ while (queue.length) {
763
+ const element = queue.shift();
764
+ if (seen.has(element)) continue;
765
+ seen.add(element);
766
+ const match = routeMatches(element, normalizedPath, defined);
767
+ if (match === null) continue;
768
+ if (match) {
769
+ showRoute(element);
770
+ element.querySelectorAll('[x-route]').forEach(nested => { if (!seen.has(nested)) queue.push(nested); });
751
771
  } else {
752
- element.setAttribute('hidden', '');
753
- element.style.display = 'none';
772
+ hideRoute(element);
754
773
  }
755
- });
774
+ }
756
775
  }
757
776
 
758
777
  // Add x-cloak to route elements that don't have it
@@ -770,9 +789,7 @@ function initializeVisibility() {
770
789
  addXCloakToRouteElements();
771
790
 
772
791
  // Process initial visibility (use logical path when app is in a subpath)
773
- const currentPath = window.ManifestRoutingNavigation?.getCurrentRoute() ?? window.location.pathname;
774
- const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
775
- processRouteVisibility(normalizedPath);
792
+ processRouteVisibility(currentNormalizedPath());
776
793
 
777
794
  // Listen for route changes
778
795
  window.addEventListener('manifest:route-change', (event) => {
@@ -785,10 +802,7 @@ function initializeVisibility() {
785
802
  if (isPrerenderedStaticMPA()) return;
786
803
  // Add x-cloak to any new route elements
787
804
  addXCloakToRouteElements();
788
-
789
- const currentPath = window.ManifestRoutingNavigation?.getCurrentRoute() ?? window.location.pathname;
790
- const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
791
- processRouteVisibility(normalizedPath);
805
+ processRouteVisibility(currentNormalizedPath());
792
806
  });
793
807
  }
794
808
 
@@ -809,8 +823,10 @@ if (document.readyState === 'loading') {
809
823
  window.ManifestRoutingVisibility = {
810
824
  initialize: initializeVisibility,
811
825
  processRouteVisibility,
826
+ isRouteActive,
812
827
  isPrerenderedStaticMPA
813
- };
828
+ };
829
+
814
830
 
815
831
  // Router head
816
832
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.198-next.3",
3
+ "version": "0.5.198",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",