stx-router 0.2.382 → 0.2.384

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.
Files changed (2) hide show
  1. package/dist/client.js +73 -9
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -182,8 +182,14 @@ function routerSource() {
182
182
  log('[router] build skew: page is',loadedBuild,'server is',incoming,'\u2014 full navigation');
183
183
  location.href=url;
184
184
  }
185
+ // A link marked data-stx-transition="none" swaps without the cross-fade. A
186
+ // tab bar is the case: iOS switches tabs instantly, and the fade read as a
187
+ // quarter-second lag on every tap in a phone app. Set by the click that
188
+ // started the navigation, so programmatic and history navigations keep it.
189
+ var instantNext=false;
190
+ var instantNav=false;
185
191
  function runViewTransition(callback){
186
- if(!o.viewTransitions||!document.startViewTransition)return false;
192
+ if(instantNav||!o.viewTransitions||!document.startViewTransition)return false;
187
193
  try{
188
194
  var transition=document.startViewTransition(callback);
189
195
  var onAbort=function(err){log('[router] view transition aborted:',err&&err.message?err.message:err)};
@@ -210,19 +216,28 @@ function routerSource() {
210
216
  progEl.style.opacity=progVal>0?'1':'0';
211
217
  progEl.style.transform='scaleX('+(progVal/100)+')';
212
218
  }
219
+ // Shown only for a navigation that is actually waiting. A page served from
220
+ // the prefetch cache swaps in a few milliseconds, and drawing then fading a
221
+ // bar for it made every instant navigation look like a load.
222
+ var progDelay=null;
213
223
  function startProgress(){
214
224
  if(!o.progress||!progEl)return;
215
225
  if(progTimer){clearInterval(progTimer);progTimer=null}
216
- setProgress(8);
217
- progTimer=setInterval(function(){
218
- if(progVal>=90)return;
219
- // Asymptotic trickle \u2014 fast at first, slower near 90%
220
- var inc=Math.max(0.4,(90-progVal)*0.08);
221
- setProgress(progVal+inc);
222
- },160);
226
+ if(progDelay)clearTimeout(progDelay);
227
+ progDelay=setTimeout(function(){
228
+ progDelay=null;
229
+ setProgress(8);
230
+ progTimer=setInterval(function(){
231
+ if(progVal>=90)return;
232
+ // Asymptotic trickle \u2014 fast at first, slower near 90%
233
+ var inc=Math.max(0.4,(90-progVal)*0.08);
234
+ setProgress(progVal+inc);
235
+ },160);
236
+ },150);
223
237
  }
224
238
  function finishProgress(){
225
239
  if(!o.progress||!progEl)return;
240
+ if(progDelay){clearTimeout(progDelay);progDelay=null;return}
226
241
  if(progTimer){clearInterval(progTimer);progTimer=null}
227
242
  setProgress(100);
228
243
  // Fade out, then reset. Delays chosen so the "full" state is briefly
@@ -472,6 +487,47 @@ function routerSource() {
472
487
  function isComposablesScript(s){
473
488
  return !!s&&((s.hasAttribute&&s.hasAttribute('data-stx-composables'))||COMPOSABLES_TEXT.test(s.textContent||''));
474
489
  }
490
+ /*
491
+ * What the block wrap below would otherwise hide (stacksjs/stx#2041).
492
+ *
493
+ * A re-executed script is wrapped in a bare block so that top-level const
494
+ * and let cannot collide across navigations. That wrap is invisible for a
495
+ * plain function declaration -- Annex B.3.3 still hoists its binding to the
496
+ * enclosing scope in sloppy mode -- and NOT invisible for anything else.
497
+ * async function, function*, class, const and let are all block-scoped, so
498
+ * the same source published a global when the browser parsed the script and
499
+ * published nothing when the router re-ran it.
500
+ *
501
+ * The effect is a divergence between development and a built app, because
502
+ * only the built one reaches a route as a fragment: a page served whole runs
503
+ * its script natively. An inline onclick that names such a declaration then
504
+ * worked in dev and threw ReferenceError in the packaged app, where there is
505
+ * no console, so it read as a button that does nothing.
506
+ *
507
+ * So: re-publish what the browser would have published. Only values that are
508
+ * functions, which is what an inline handler calls -- assigning every
509
+ * top-level const would otherwise write over window.name and friends, which
510
+ * a lexical global shadows rather than overwrites. Each name is guarded on
511
+ * its own so an unmatched one cannot take the rest down, and typeof is safe
512
+ * on a name that was never declared.
513
+ *
514
+ * Column-anchored: a declaration nested inside a function is indented, and
515
+ * is not in scope at the end of the block anyway.
516
+ */
517
+ var TOP_LEVEL_DECL=/^(?:export\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm;
518
+ function republishTopLevel(src){
519
+ var names=[],m;
520
+ TOP_LEVEL_DECL.lastIndex=0;
521
+ while((m=TOP_LEVEL_DECL.exec(src))!==null){
522
+ if(names.indexOf(m[1])===-1)names.push(m[1]);
523
+ }
524
+ if(!names.length)return '';
525
+ var out='\\n;';
526
+ for(var i=0;i<names.length;i++){
527
+ out+='try{if(typeof '+names[i]+'==="function")window.'+names[i]+'='+names[i]+'}catch(e){}';
528
+ }
529
+ return out;
530
+ }
475
531
  function runsAlways(declared, code){
476
532
  if(declared==='always')return true;
477
533
  if(declared==='once')return false;
@@ -783,6 +839,8 @@ function routerSource() {
783
839
  if(pushState!==false&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
784
840
 
785
841
  isNavigating=true;
842
+ instantNav=instantNext;
843
+ instantNext=false;
786
844
  document.body.classList.add(o.loadingClass);
787
845
  startProgress();
788
846
 
@@ -1318,6 +1376,7 @@ else {
1318
1376
  }catch(err){reject(err)}
1319
1377
  }
1320
1378
  if(runViewTransition(completeFragSwap)){}
1379
+ else if(instantNav){completeFragSwap()}
1321
1380
  else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
1322
1381
  });
1323
1382
  }
@@ -1732,7 +1791,7 @@ else {
1732
1791
  // ESM, and top-level 'import' is illegal inside a block, which
1733
1792
  // would throw SyntaxError before the script ever runs.
1734
1793
  var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
1735
- ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
1794
+ ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+republishTopLevel(text)+'}';
1736
1795
  ns.setAttribute('data-stx-page','');
1737
1796
  var placeholder=entry.slot?qs('script[data-stx-route-script="'+entry.slot+'"]'):null;
1738
1797
  if(placeholder&&placeholder.parentNode){
@@ -1768,6 +1827,10 @@ else {
1768
1827
  }
1769
1828
  if(runViewTransition(completeSwap)){
1770
1829
  }
1830
+ else if(instantNav){
1831
+ // A tab: swap at once, no fade either way.
1832
+ completeSwap();
1833
+ }
1771
1834
  else {
1772
1835
  // Fallback fade for browsers without View Transitions API
1773
1836
  currentContent.style.transition='opacity 0.12s ease-out';
@@ -1886,6 +1949,7 @@ else {
1886
1949
  e.preventDefault();
1887
1950
  e.stopPropagation();
1888
1951
  log('[router] navigating to:',href);
1952
+ instantNext=link.getAttribute('data-stx-transition')==='none';
1889
1953
  navigate(withCurrentLocale(href));
1890
1954
  });
1891
1955
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.382",
4
+ "version": "0.2.384",
5
5
  "description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",