stx-router 0.2.290 → 0.2.292

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 +156 -48
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ export function getRouterScript() {
16
16
  if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
17
17
 
18
18
  // \u2500\u2500 Configuration \u2500\u2500
19
- var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false};
19
+ var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500};
20
20
  var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
21
21
  var containerSel=o.container;
22
22
  var debug=!!o.debug;
@@ -780,6 +780,99 @@ else {
780
780
  try{target.focus({preventScroll:true})}catch(e){try{target.focus()}catch(e2){}}
781
781
  }
782
782
 
783
+ // \u2500\u2500 Generated utility stylesheets \u2500\u2500
784
+ // Every page ships its own content-addressed utility sheet as
785
+ // <link data-css="generated">, and none of them is ever removed on a
786
+ // navigation, so they pile up in <head>. Two of them can define the same
787
+ // class at the same specificity, which makes their ORDER decide the cascade:
788
+ // the later sheet wins. Deduping alone left that order as "first seen", so
789
+ // after Records \u2192 Feed \u2192 Records the Feed sheet still sat after the Records
790
+ // one and its .flex-col beat Records' own responsive sm:flex-row \u2014 the header
791
+ // stacked into a column on desktop. The current page's complete sheet has to
792
+ // be the last one, every navigation, not just the first time it is seen.
793
+ function findGeneratedCss(abs){
794
+ var found=null;
795
+ document.querySelectorAll('head link[data-css][href]').forEach(function(link){
796
+ if(new URL(link.getAttribute('href'),location.href).href!==abs)return;
797
+ // Collapse duplicates, so moving one copy to the end can never leave an
798
+ // older copy of the same sheet behind to fight it.
799
+ if(found){link.parentNode.removeChild(link);return}
800
+ found=link;
801
+ });
802
+ return found;
803
+ }
804
+
805
+ // Before the swap: add each sheet the page does not have yet, as
806
+ // media="print" so it downloads without restyling the page still on screen.
807
+ // Returns a promise for them to finish (load OR error, capped at
808
+ // cssLoadTimeout so a slow or missing sheet never stalls navigation), or null
809
+ // when nothing new is needed and the swap can go ahead in the same turn.
810
+ function preloadGeneratedCss(hrefs,base){
811
+ var pending=[];
812
+ hrefs.forEach(function(href){
813
+ var abs;
814
+ try{abs=new URL(href,base).href}catch(e){return}
815
+ var link=findGeneratedCss(abs);
816
+ if(link){
817
+ // Still downloading for a navigation that never swapped \u2014 wait on it
818
+ // rather than on nothing.
819
+ if(link.__stxCssReady)pending.push(link.__stxCssReady);
820
+ return;
821
+ }
822
+ link=document.createElement('link');
823
+ link.setAttribute('data-css','generated');
824
+ link.setAttribute('rel','stylesheet');
825
+ link.setAttribute('href',new URL(href,location.href).href===abs?href:abs);
826
+ link.setAttribute('media','print');
827
+ link.setAttribute('data-stx-css-pending','');
828
+ link.__stxCssReady=new Promise(function(resolve){
829
+ var timer=setTimeout(finish,o.cssLoadTimeout);
830
+ function finish(){clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;resolve()}
831
+ link.onload=finish;
832
+ link.onerror=finish;
833
+ });
834
+ pending.push(link.__stxCssReady);
835
+ document.head.appendChild(link);
836
+ });
837
+ return pending.length?Promise.all(pending):null;
838
+ }
839
+
840
+ // At the swap: make the destination's sheets live and move them to the end
841
+ // of <head>. appendChild on a node that is already there MOVES it \u2014 no
842
+ // duplicate, and the browser keeps the parsed sheet rather than refetching.
843
+ // Only generated sheets are reordered; app and font stylesheets keep the
844
+ // order they were declared in.
845
+ function promoteGeneratedCss(hrefs,base){
846
+ hrefs.forEach(function(href){
847
+ var abs;
848
+ try{abs=new URL(href,base).href}catch(e){return}
849
+ var link=findGeneratedCss(abs);
850
+ if(!link){
851
+ preloadGeneratedCss([href],base);
852
+ link=findGeneratedCss(abs);
853
+ if(!link)return;
854
+ }
855
+ if(link.hasAttribute('data-stx-css-pending')){
856
+ link.removeAttribute('data-stx-css-pending');
857
+ link.removeAttribute('media');
858
+ }
859
+ document.head.appendChild(link);
860
+ });
861
+ }
862
+
863
+ // The generated-sheet hrefs a fragment carries. Scripts are skipped so a
864
+ // <link> written inside a string in page code is not mistaken for one.
865
+ function fragmentCssHrefs(html){
866
+ var hrefs=[];
867
+ html.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'').replace(new RegExp('<link\\\\b([^>]*)>','gi'),function(m,attrs){
868
+ if(attrs.indexOf('data-css')===-1)return m;
869
+ var hrefMatch=attrs.match(/\\bhref=(["'])(.*?)\\1/i);
870
+ if(hrefMatch&&hrefMatch[2])hrefs.push(hrefMatch[2]);
871
+ return m;
872
+ });
873
+ return hrefs;
874
+ }
875
+
783
876
  function swap(html,url,pushState,hash){
784
877
  var fragMark=/^<!--stx-fragment(?: rt=([01]))?-->/.exec(html);
785
878
  var isFragment=!!fragMark;
@@ -800,7 +893,6 @@ else {
800
893
  location.href=url;
801
894
  return Promise.resolve(false);
802
895
  }
803
- if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
804
896
  function doFragSwap(){
805
897
  // Extract scripts from fragment before injecting HTML
806
898
  var fragScripts=[];
@@ -877,19 +969,9 @@ else {
877
969
  document.head.appendChild(cw);
878
970
  }
879
971
  }
880
- fragCssHrefs.forEach(function(href){
881
- var absolute=new URL(href,location.href).href;
882
- var exists=false;
883
- document.querySelectorAll('head link[data-css][href]').forEach(function(link){
884
- if(new URL(link.getAttribute('href'),location.href).href===absolute)exists=true;
885
- });
886
- if(exists)return;
887
- var link=document.createElement('link');
888
- link.setAttribute('data-css','generated');
889
- link.setAttribute('rel','stylesheet');
890
- link.setAttribute('href',href);
891
- document.head.appendChild(link);
892
- });
972
+ // Already present ones are MOVED last, not skipped \u2014 see
973
+ // promoteGeneratedCss for why their order is the whole bug.
974
+ promoteGeneratedCss(fragCssHrefs,location.href);
893
975
  // Add new page styles
894
976
  fragStyles.forEach(function(s){
895
977
  var el=document.createElement('style');
@@ -1008,16 +1090,25 @@ else {
1008
1090
  if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
1009
1091
  else runFragScripts();
1010
1092
  }
1011
- return new Promise(function(resolve,reject){
1012
- function completeFragSwap(){
1013
- try{
1014
- doFragSwap();
1015
- resolve(true);
1016
- }catch(err){reject(err)}
1017
- }
1018
- if(runViewTransition(completeFragSwap)){}
1019
- 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)}
1020
- });
1093
+ function startFragSwap(){
1094
+ if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
1095
+ return new Promise(function(resolve,reject){
1096
+ function completeFragSwap(){
1097
+ try{
1098
+ doFragSwap();
1099
+ resolve(true);
1100
+ }catch(err){reject(err)}
1101
+ }
1102
+ if(runViewTransition(completeFragSwap)){}
1103
+ 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)}
1104
+ });
1105
+ }
1106
+ // A stylesheet this page has never loaded is fetched BEFORE the content
1107
+ // swaps, or the new markup paints once without its utilities and then
1108
+ // jumps. Cleanup waits with it so the outgoing page stays live meanwhile.
1109
+ // With nothing new to load this runs in the same turn, as it always did.
1110
+ var fragCssReady=preloadGeneratedCss(fragmentCssHrefs(html),location.href);
1111
+ return fragCssReady?fragCssReady.then(startFragSwap):startFragSwap();
1021
1112
  }
1022
1113
 
1023
1114
  // Full document mode: parse with DOMParser and extract container content
@@ -1047,10 +1138,13 @@ else {
1047
1138
  var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
1048
1139
  if(!newContent){location.href=url;return Promise.resolve(false)}
1049
1140
 
1050
- // Clean up existing signals/effects
1051
- if(window.stx&&window.stx._cleanupContainer){
1052
- window.stx._cleanupContainer(currentContent);
1053
- }
1141
+ // The destination's generated utility sheets, resolved against the page
1142
+ // being navigated to. Loaded before the swap and moved last during it \u2014
1143
+ // same reasons as the fragment path (promoteGeneratedCss).
1144
+ var docCssBase;
1145
+ try{docCssBase=new URL(url,location.href).href}catch(e){docCssBase=location.href}
1146
+ var docCssHrefs=[];
1147
+ doc.querySelectorAll('head link[data-css][href]').forEach(function(l){docCssHrefs.push(l.getAttribute('href'))});
1054
1148
 
1055
1149
  function doSwap(){
1056
1150
  // \u2500\u2500 Swap <head> styles \u2500\u2500
@@ -1117,6 +1211,9 @@ else {
1117
1211
  doc.querySelectorAll('head '+linkSel).forEach(function(l){
1118
1212
  var href=l.getAttribute('href');
1119
1213
  if(!href)return;
1214
+ // Generated utility sheets are not additive-only: promoteGeneratedCss
1215
+ // below also reorders them.
1216
+ if(l.hasAttribute('data-css'))return;
1120
1217
  var abs=new URL(href,docBase).href;
1121
1218
  if(curLinks[abs])return;
1122
1219
  curLinks[abs]=1;
@@ -1124,6 +1221,9 @@ else {
1124
1221
  Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
1125
1222
  document.head.appendChild(nl);
1126
1223
  });
1224
+ // After every other stylesheet link, so the destination's complete
1225
+ // utility sheet is last in the cascade.
1226
+ promoteGeneratedCss(docCssHrefs,docBase);
1127
1227
 
1128
1228
  // \u2500\u2500 Swap content \u2500\u2500
1129
1229
  // For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
@@ -1417,26 +1517,34 @@ else {
1417
1517
  }
1418
1518
  }
1419
1519
 
1420
- return new Promise(function(resolve,reject){
1421
- function completeSwap(){
1422
- try{
1423
- doSwap();
1424
- resolve(true);
1425
- }catch(err){reject(err)}
1426
- }
1427
- if(runViewTransition(completeSwap)){
1428
- }
1429
- else {
1430
- // Fallback fade for browsers without View Transitions API
1431
- currentContent.style.transition='opacity 0.12s ease-out';
1432
- currentContent.style.opacity='0';
1433
- setTimeout(function(){
1434
- completeSwap();
1435
- currentContent.style.opacity='1';
1436
- setTimeout(function(){currentContent.style.transition=''},150);
1437
- },120);
1520
+ function startSwap(){
1521
+ // Clean up existing signals/effects
1522
+ if(window.stx&&window.stx._cleanupContainer){
1523
+ window.stx._cleanupContainer(currentContent);
1438
1524
  }
1439
- });
1525
+ return new Promise(function(resolve,reject){
1526
+ function completeSwap(){
1527
+ try{
1528
+ doSwap();
1529
+ resolve(true);
1530
+ }catch(err){reject(err)}
1531
+ }
1532
+ if(runViewTransition(completeSwap)){
1533
+ }
1534
+ else {
1535
+ // Fallback fade for browsers without View Transitions API
1536
+ currentContent.style.transition='opacity 0.12s ease-out';
1537
+ currentContent.style.opacity='0';
1538
+ setTimeout(function(){
1539
+ completeSwap();
1540
+ currentContent.style.opacity='1';
1541
+ setTimeout(function(){currentContent.style.transition=''},150);
1542
+ },120);
1543
+ }
1544
+ });
1545
+ }
1546
+ var docCssReady=preloadGeneratedCss(docCssHrefs,docCssBase);
1547
+ return docCssReady?docCssReady.then(startSwap):startSwap();
1440
1548
  }
1441
1549
 
1442
1550
  // \u2500\u2500 Link interception \u2500\u2500
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.290",
4
+ "version": "0.2.292",
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",