@xingwangzhe/stalux 1.56.0 → 1.58.0

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 (44) hide show
  1. package/package.json +4 -12
  2. package/src/components/stalux/archives/archivesList.astro +1 -1
  3. package/src/components/stalux/categories/categoriesCard.astro +1 -1
  4. package/src/components/stalux/posts/PostTaxonomy.astro +1 -1
  5. package/src/components/stalux/posts/postCard.astro +3 -3
  6. package/src/components/stalux/tags/tagHeader.astro +2 -2
  7. package/src/components/stalux/tags/tagsCloud.astro +2 -2
  8. package/src/index.ts +39 -13
  9. package/src/internal/html-output.ts +18 -0
  10. package/src/internal/modern-css.ts +1 -0
  11. package/src/internal/route-hrefs.ts +47 -0
  12. package/src/internal/social-card-data.ts +1 -1
  13. package/src/internal/style-names.generated.ts +87 -94
  14. package/src/internal/swup-options.ts +98 -0
  15. package/src/layouts/Stalux.astro +0 -1
  16. package/src/pages/404.astro +1 -1
  17. package/src/pages/api/posts.json.ts +1 -1
  18. package/src/pages/atom.xml.ts +2 -0
  19. package/src/pages/posts/[post].md.ts +2 -2
  20. package/src/pages/rss.xml.ts +2 -0
  21. package/src/scripts/analytics-runtime.ts +4 -9
  22. package/src/scripts/list-reveal.ts +3 -15
  23. package/src/scripts/module-motion.ts +2 -3
  24. package/src/scripts/navigation-scripts.ts +12 -7
  25. package/src/scripts/route-guards.ts +200 -0
  26. package/src/scripts/swup-internals.d.ts +15 -0
  27. package/src/styles/base.css +0 -8
  28. package/src/styles/generated-prose.css +1 -1
  29. package/src/styles/generated-waline.css +1 -1
  30. package/src/styles/generated.css +1 -1
  31. package/src/styles/integrations.css +14 -16
  32. package/src/styles/math.css +3 -1
  33. package/src/styles/motion.css +1 -22
  34. package/src/utils/accent-color.ts +52 -0
  35. package/src/utils/ai-discovery.ts +1 -1
  36. package/src/utils/feed.ts +1 -1
  37. package/src/utils/public-routes.ts +14 -4
  38. package/src/utils/seo-document.ts +3 -2
  39. package/src/scripts/route-motion.ts +0 -340
  40. package/src/scripts/route-out.ts +0 -63
  41. package/src/scripts/smooth-scroll.ts +0 -139
  42. package/src/scripts/swup-router.ts +0 -580
  43. package/src/scripts/view-transitions.ts +0 -4
  44. package/src/utils/route-motion-policy.ts +0 -72
@@ -1,3 +1,3 @@
1
1
  @layer theme, base, components, utilities;
2
- @layer components{.stalux-ai-prompt{clip-path:inset(50%);white-space:pre-wrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.stalux-badge-group{border:none}.stalux-badge-group-arrow{font-size:.65rem;transition:transform .2s;display:inline-block}.stalux-badge-group-summary{user-select:none;cursor:pointer;color:var(--white-70p);gap:.4rem;padding:.2rem 0;font-size:.85rem;list-style-type:none;transition:color .2s}.stalux-beian-public-security-beian{color:#ffffffb3;gap:5px;font-size:.9rem;text-decoration:none;transition:color .3s;display:inline-flex}.stalux-card-time{gap:.45rem;margin:0;display:inline-flex}.stalux-copyright{color:var(--white-70p);gap:.3em;font-weight:500}.stalux-footer{backdrop-filter:blur(10px);color:var(--white-full);background-color:#0000001a;margin-top:auto;padding:1.5rem 0;font-size:.9rem}.stalux-footer-content{gap:.8rem;max-width:1200px;margin:0 auto;padding:0 1rem}.stalux-nav-list{transform:scale(var(--nav-scale,1));transform-origin:50%;box-sizing:border-box;gap:2rem;max-width:100%;transition:transform .22s cubic-bezier(.22,.72,.22,1);overflow:visible}.stalux-nav-overlay{pointer-events:none;z-index:999;opacity:0;background:#0009;height:100%;transition:opacity .3s;display:none;position:fixed;top:0;left:0}.stalux-nav-scroll-btn{background:var(--black-30p);backdrop-filter:blur(8px);box-shadow:var(--shadow-md);pointer-events:none;z-index:10;cursor:pointer;width:36px;height:36px;color:var(--white-full);opacity:0;border:none;border-radius:9999px;padding:0;transition:opacity .25s;display:none;position:absolute;top:50%;transform:translateY(-50%)}.stalux-nav-toggle{z-index:998;cursor:pointer;min-width:48px;min-height:48px;color:inherit;background:0 0;border:none;padding:.75rem;display:none}.stalux-stats-site-build-time{color:#fffc;margin:.5rem 0 0;font-size:.9rem}.stalux-stats-stats-separator{color:#ffffff80}.stalux-theme-info{color:var(--white-70p);gap:.3em;margin:0;font-size:.85rem}.stalux-nav-container{box-sizing:border-box;max-width:100%;position:relative;overflow:hidden}.stalux-beian-beian-info{gap:1rem;margin:0}.stalux-nav-item{text-shadow:var(--stalux-text-shadow);flex-shrink:0;margin-left:1em;margin-right:1em;font-size:1.3em}.stalux-page-description{color:var(--white-60p);margin-top:0;margin-bottom:1.5em;font-size:1.2em}.stalux-page-title{text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:2.5em}.stalux-badge,.stalux-badges-wrapper .stalux-badge{width:auto}.stalux-u-min-w-0{min-width:0}.stalux-u-w-full{width:100%}.stalux-u-flex{display:flex}.stalux-u-flex-col{flex-direction:column}.stalux-u-flex-wrap{flex-wrap:wrap}.stalux-u-items-center{align-items:center}.stalux-u-justify-center{justify-content:center}.stalux-badges,.stalux-badges-wrapper{gap:.5rem}.stalux-stats-site-stats{gap:.5rem;font-size:.9rem}.stalux-nav-text{white-space:nowrap;color:var(--white-full)}.stalux-badge-group-items{padding-top:.35rem}.stalux-u-text-center{text-align:center}.stalux-badge-group-hint{color:var(--white-70p);font-size:.7rem}.stalux-badge-group-title{color:var(--white-80p)}.stalux-weather-heading{font-size:inherit;font-weight:inherit;margin:0}nav{width:100%;min-width:0;max-width:100%}html{scroll-padding-top:var(--stalux-nav-offset,var(--stalux-site-header-height))}.stalux-site-header{z-index:990;width:100%;min-height:var(--stalux-site-header-min-height);isolation:isolate;justify-content:center;align-items:center;display:flex;position:sticky;top:0}.stalux-nav-scroll-btn[data-visible]{pointer-events:auto;opacity:1}.stalux-nav-scroll-btn[data-side=left]{left:4px}.stalux-nav-scroll-btn[data-side=right]{right:4px}.stalux-nav-scroll-btn svg{stroke:currentColor;stroke-width:2px;fill:none;width:20px;height:20px}.stalux-nav-link,.stalux-nav-link:visited{cursor:pointer;width:100%;color:inherit;min-height:44px;font:inherit;background:0 0;border:none;align-items:center;gap:.5rem;padding:.5rem .75rem;text-decoration:none;display:flex}.stalux-nav-link .nav-icon{stroke:var(--white-full)}.stalux-nav-toggle svg{width:32px;height:32px}.stalux-nav-overlay[data-state=show]{pointer-events:auto;opacity:1;display:block}#stalux-to-archive-archs{margin:2em 0 0}#stalux-date-date-time{text-shadow:var(--stalux-text-shadow);color:var(--white-full);flex-direction:column;align-items:center;margin:2em 0 0;display:flex}#stalux-date-date-time #stalux-date-date{min-height:1.25em;margin-bottom:.5em;font-size:2em;line-height:1.25}#stalux-date-date-time #stalux-date-time{font-variant-numeric:tabular-nums;min-height:1.25em;font-size:3em;line-height:1.25}.stalux-theme-info span,.stalux-copyright span{margin:0}.stalux-theme-info a{color:#fffc;margin:0 .2em;text-decoration:none;transition:color .3s}.stalux-beian-beian-info a{color:#ffffffb3;font-size:.9rem;text-decoration:none;transition:color .3s}.stalux-beian-public-security-beian img{width:15px;height:15px;display:inline-block}.stalux-badge-group-summary::-webkit-details-marker,.stalux-badge-group[open] .stalux-badge-group-hint[data-hint=collapsed],.stalux-badge-group:not([open]) .stalux-badge-group-hint[data-hint=expanded]{display:none}.stalux-badge-group[open] .stalux-badge-group-arrow{transform:rotate(90deg)}@media (hover:hover) and (pointer:fine){.stalux-nav-scroll-btn:hover{background:0 0}.stalux-nav-container:hover .stalux-nav-scroll-btn[data-visible]{opacity:1}.stalux-nav-link:hover .nav-icon,.stalux-nav-link:hover .stalux-nav-text{stroke:var(--stalux-accent-color);color:var(--stalux-accent-color)}.stalux-theme-info a:hover{color:#fff;text-decoration:none}.stalux-beian-beian-info a:hover,.stalux-beian-public-security-beian:hover{color:#ffffffe6;text-decoration:none}.stalux-badge-group-summary:hover{color:var(--white-70p)}}@media (width>=601px){.stalux-site-nav{isolation:isolate;border-radius:0 0 16px 16px;transition:background-color .18s}.stalux-site-nav[data-scrolled]{backdrop-filter:blur(20px)saturate(140%);box-shadow:0 1px 0 var(--white-10p), 0 8px 24px #0000001f;background:#181818c7}}@media (width<=1100px){.stalux-nav-list{gap:1rem}.stalux-nav-item{margin-left:.6em;margin-right:.6em;font-size:1em}}@media (width<=900px){.stalux-nav-item{margin-left:.25em;margin-right:.25em;font-size:.95em}.stalux-nav-list{gap:.5rem}}@media (width<=768px){.stalux-footer{padding:1.2rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}.stalux-badges{gap:.3rem}.stalux-badge{width:auto}}@media (width<=600px){html{--stalux-site-header-min-height:4rem}.stalux-nav-toggle[data-state=open]{visibility:hidden;pointer-events:none}.stalux-nav-toggle[data-scrolled]{backdrop-filter:blur(20px)saturate(140%);box-shadow:0 1px 0 var(--white-10p), 0 8px 24px #0000001f;background:#181818c7;border-radius:12px}.stalux-nav-toggle{display:block;position:fixed;top:.75rem;left:.75rem}.stalux-nav-container{overflow:visible}nav{width:auto}.stalux-nav-list{pointer-events:none;z-index:1000;background:var(--black-20p);backdrop-filter:blur(20px);flex-direction:column;justify-content:flex-start;align-items:flex-start;gap:0;width:min(220px,100vw - 1rem);height:100vh;padding:.5rem 1rem 2rem;position:fixed;top:0;left:0;right:auto;overflow-y:auto;transform:translate(-100%)}.stalux-nav-list[data-state=open]{pointer-events:auto;transform:translate(0)}.stalux-nav-item{border-bottom:1px solid var(--white-10p);flex-shrink:0;width:100%;margin:0;font-size:.9em}.stalux-nav-text{display:inline}.stalux-nav-link{width:100%;min-height:44px;font-size:inherit;padding:.45rem .75rem}.stalux-nav-link .nav-icon{width:18px;height:18px}}@media (width<=480px){.stalux-footer{padding:1rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}}.stalux-main,.stalux-post-page,.stalux-post-layout>*,.stalux-link-card-content,.stalux-word-list-item,.stalux-word-card{box-sizing:border-box;min-width:0;max-width:100%}.stalux-main{overflow-wrap:anywhere}.stalux-link-shell{min-width:0;max-width:100%;display:block}.stalux-link-card{box-sizing:border-box;width:100%;max-width:100%}.stalux-category-shell{width:100%;min-width:0}.stalux-author-figure>a{flex-shrink:0;display:inline-block}.stalux-author-figure img{display:block}.stalux-post-card-shell,.stalux-category-shell,.stalux-link-shell{position:relative}.stalux-post-card,.stalux-category-card,.stalux-link-card,.stalux-word-card{transition:transform .2s,background-color .2s,border-color .2s,box-shadow .2s}:is(.stalux-post-card-shell,.stalux-category-shell):focus-within>*,.stalux-link-shell:focus-visible>*{box-shadow:var(--card-box-shadow-hover)}:is(button,summary,[role=button]):focus-visible{outline:2px solid var(--stalux-accent-color);outline-offset:3px}@media (prefers-reduced-motion:reduce){.stalux-main *,.stalux-main :before,.stalux-main :after{transition-duration:0s!important;animation:none!important}.stalux-post-card,.stalux-category-card,.stalux-link-card,.stalux-link-card-content,.stalux-card-post-cover img,.stalux-category-icon,.stalux-author-figure img,.stalux-link-card-avatar img{transform:none!important}}@keyframes stalux-social-stalux-social-arrive{0%{opacity:0;transform:translateY(.75rem)rotate(-12deg)}to{opacity:1;transform:translateY(0)rotate(0)}}@keyframes stalux-random-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes stalux-link-avatar-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}}@layer utilities{.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visible{visibility:visible}.absolute{position:absolute}.static{position:static}.grid{display:grid}.block{display:block}.inline-block{display:inline-block}.hidden{display:none}.h-full{height:100%}.flex{display:flex}.flex-shrink{flex-shrink:1}.flex-wrap{flex-wrap:wrap}.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))}.b,.border{border-width:1px}.text-inherit{color:inherit}.no-underline{text-decoration:none}.outline{outline-style:solid}}@layer theme;@layer base{html,body{color:#fff;width:100%;height:100%;margin:0;& main{flex:1}scrollbar-color:var(--stalux-scrollbar-thumb) var(--stalux-scrollbar-track);scrollbar-width:auto;font-family:var(--font-body), "Noto Sans SC", "Noto Sans CJK SC", system-ui, -apple-system, sans-serif;text-rendering:optimizelegibility;line-height:1.25;display:block}body{height:auto;min-height:100%}html{scrollbar-gutter:stable;background-color:var(--stalux-bg-color,#202020)}html[data-clarity-id]{opacity:1!important}a{color:inherit;cursor:pointer;text-decoration-skip-ink:auto;background-color:#0000;text-decoration:none;transition:color .2s,text-shadow .2s;position:relative}.a-none{cursor:pointer;text-decoration-skip-ink:auto;background-color:#0000;color:inherit!important;text-decoration:none!important}.a-none:after{display:none!important}a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):after{content:"";background:linear-gradient(90deg, var(--stalux-accent-color), var(--accent-50p));transform-origin:0;pointer-events:none;width:100%;height:1px;transition:transform .25s cubic-bezier(.25,.9,.2,1);position:absolute;bottom:-2px;left:0;transform:scaleX(0)}a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):focus-visible:after,a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):active:after{transform:scaleX(1)}@media (hover:hover) and (pointer:fine){a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):hover:after{transform:scaleX(1)}}a:active,a:focus-visible{text-decoration:none}@media (hover:hover) and (pointer:fine){a:hover{text-decoration:none}}a:focus{outline:none}a:focus-visible{outline:2px solid var(--stalux-accent-color);outline-offset:2px}ul{margin:0;padding:0;list-style:none}.none{display:none}.stalux-root{color:#fff;min-height:100vh}.stalux-main{min-height:calc(100vh - var(--stalux-site-header-height,3rem))}:root{--stalux-bg-color:#202020;--stalux-bg-filter:none;--stalux-scrollbar-thumb:#7d7d7d;--stalux-scrollbar-thumb-hover:#9a9a9a;--stalux-scrollbar-track:#2a2a2a;--stalux-site-header-min-height:3.75rem;--stalux-site-header-height:var(--stalux-site-header-min-height);--stalux-text-shadow:var(--stalux-accent-color) .08rem .08rem .18rem;--stalux-accent-color:#eab308;--stalux-accent-rgb:234 179 8;--surface-glass:var(--black-05p);--surface-glass-shadow:var(--shadow-sidebar);--card-bg:var(--surface-glass);--card-border:#ffffff1a;--card-box-shadow:0 10px 24px #00000014;--card-box-shadow-hover:0 14px 28px #0000001f;--tag-bg:#ffffff1f;--tag-bg-hover:#ffffff2e;--category-bg:#ffffff14;--category-bg-hover:#ffffff24;--date-bg:#42a0ff2e;--date-border:#42a0ff40;--updated-bg:#ffc94733;--updated-border:#ffc9474d;--white-full:#fff;--white-95p:#fffffff2;--white-90p:#ffffffe6;--white-85p:#ffffffd9;--white-80p:#ffffffe6;--white-78p:#ffffffc7;--white-75p:#ffffffbf;--white-70p:#ffffffdb;--white-60p:#fff9;--white-50p:#ffffff80;--white-30p:#ffffff4d;--white-20p:#fff3;--white-15p:#ffffff26;--white-10p:#ffffff1a;--white-05p:#ffffff0d;--black-30p:#0000004d;--black-20p:#0003;--black-15p:#00000026;--black-10p:#0000001a;--black-05p:#0000000d;--accent-90p:rgb(var(--stalux-accent-rgb) / .9);--accent-85p:rgb(var(--stalux-accent-rgb) / .85);--accent-80p:rgb(var(--stalux-accent-rgb) / .8);--accent-70p:rgb(var(--stalux-accent-rgb) / .7);--accent-60p:rgb(var(--stalux-accent-rgb) / .6);--accent-50p:rgb(var(--stalux-accent-rgb) / .5);--accent-40p:rgb(var(--stalux-accent-rgb) / .4);--accent-30p:rgb(var(--stalux-accent-rgb) / .3);--accent-20p:rgb(var(--stalux-accent-rgb) / .2);--transition-fast:.18s ease;--transition-medium:.2s ease;--transition-normal:.25s ease;--transition-slow:.3s ease;--transition-slower:.5s ease;--transition-slowest:.6s ease;--easing-bounce:cubic-bezier(.175, .885, .32, 1.275);--space-2xs:.25rem;--space-xs:.75rem;--space-sm:.5rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--space-3xl:4rem;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-full:9999px;--shadow-sm:0 2px 5px #0000001a;--shadow-md:0 6px 16px #0003;--shadow-lg:0 8px 20px #0000004d;--shadow-xl:0 12px 26px #00000047;--shadow-sidebar:0 16px 20px -12px #000000d6, 10px 4px 16px -12px #00000094, -10px 4px 16px -12px #00000094;--shadow-raised-hover:0 20px 24px -13px #000000e6, 12px 5px 18px -13px #000000ad, -12px 5px 18px -13px #000000ad;--shadow-dark:#000000c0;--muted-text:#bbb;--font-body:"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;--font-size-subtitle:1.35rem;--font-size-base:1.1rem;--font-size-small:.95rem;--font-size-smaller:.9rem;--font-size-tiny:.85rem;--border-thin:1px;--border-medium:2px;--border-thick:3px}@supports not (scrollbar-color:auto){::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--stalux-scrollbar-track);border-radius:4px}::-webkit-scrollbar-thumb{background:var(--stalux-scrollbar-thumb);border-radius:4px;transition:background .3s ease-out}@media (hover:hover) and (pointer:fine){::-webkit-scrollbar-thumb:hover{background:var(--stalux-scrollbar-thumb-hover)}}::-webkit-scrollbar-corner{background:var(--stalux-scrollbar-track)}}.bg-layer{z-index:-10;background-position:50%;background-repeat:repeat;background-size:auto;background-color:var(--stalux-bg-color,transparent);filter:var(--stalux-bg-filter,none);pointer-events:none;opacity:0;forced-color-adjust:none;color-scheme:light only;transition:opacity .2s ease-out;position:fixed;inset:0}@media (prefers-reduced-motion:reduce){.bg-layer{transition-duration:0s}}footer{padding:var(--space-lg) var(--space-md);text-align:center;color:var(--muted-text);border-top:1px solid var(--white-10p);backdrop-filter:blur(16px);background:linear-gradient(#0c0e122e,#0c0e12b8)}}@media print{[data-stalux-offscreen-code],[data-stalux-offscreen-svg]{content-visibility:visible!important;contain-intrinsic-block-size:none!important}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}[data-stalux-home-content]{flex-direction:column;align-items:center;width:100%;display:flex}[data-stalux-scroll-sentinel]{visibility:hidden;pointer-events:none;width:1px;height:1px;position:absolute;top:240px;left:0}@media (prefers-reduced-motion:reduce){.stalux-main,.stalux-main *,.postLayout *{scroll-behavior:auto!important;transition-duration:1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important}}
2
+ @layer components{.stalux-ai-prompt{clip-path:inset(50%);white-space:pre-wrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.stalux-badge-group{border:none}.stalux-badge-group-arrow{font-size:.65rem;transition:transform .2s;display:inline-block}.stalux-badge-group-summary{user-select:none;cursor:pointer;color:var(--white-70p);gap:.4rem;padding:.2rem 0;font-size:.85rem;list-style-type:none;transition:color .2s}.stalux-beian-public-security-beian{color:#ffffffb3;gap:5px;font-size:.9rem;text-decoration:none;transition:color .3s;display:inline-flex}.stalux-card-time{gap:.45rem;margin:0;display:inline-flex}.stalux-copyright{color:var(--white-70p);gap:.3em;font-weight:500}.stalux-footer{backdrop-filter:blur(10px);color:var(--white-full);background-color:#0000001a;margin-top:auto;padding:1.5rem 0;font-size:.9rem}.stalux-footer-content{gap:.8rem;max-width:1200px;margin:0 auto;padding:0 1rem}.stalux-nav-list{transform:scale(var(--nav-scale,1));transform-origin:50%;box-sizing:border-box;gap:2rem;max-width:100%;transition:transform .22s cubic-bezier(.22,.72,.22,1);overflow:visible}.stalux-nav-overlay{pointer-events:none;z-index:999;opacity:0;background:#0009;height:100%;transition:opacity .3s;display:none;position:fixed;top:0;left:0}.stalux-nav-scroll-btn{background:var(--black-30p);backdrop-filter:blur(8px);box-shadow:var(--shadow-md);pointer-events:none;z-index:10;cursor:pointer;width:36px;height:36px;color:var(--white-full);opacity:0;border:none;border-radius:9999px;padding:0;transition:opacity .25s;display:none;position:absolute;top:50%;transform:translateY(-50%)}.stalux-nav-toggle{z-index:998;cursor:pointer;min-width:48px;min-height:48px;color:inherit;background:0 0;border:none;padding:.75rem;display:none}.stalux-stats-site-build-time{color:#fffc;margin:.5rem 0 0;font-size:.9rem}.stalux-stats-stats-separator{color:#ffffff80}.stalux-theme-info{color:var(--white-70p);gap:.3em;margin:0;font-size:.85rem}.stalux-nav-container{box-sizing:border-box;max-width:100%;position:relative;overflow:hidden}.stalux-beian-beian-info{gap:1rem;margin:0}.stalux-nav-item{text-shadow:var(--stalux-text-shadow);flex-shrink:0;margin-left:1em;margin-right:1em;font-size:1.3em}.stalux-page-description{color:var(--white-60p);margin-top:0;margin-bottom:1.5em;font-size:1.2em}.stalux-page-title{text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:2.5em}.stalux-badge,.stalux-badges-wrapper .stalux-badge{width:auto}.stalux-u-min-w-0{min-width:0}.stalux-u-w-full{width:100%}.stalux-u-flex{display:flex}.stalux-u-flex-col{flex-direction:column}.stalux-u-flex-wrap{flex-wrap:wrap}.stalux-u-items-center{align-items:center}.stalux-u-justify-center{justify-content:center}.stalux-badges,.stalux-badges-wrapper{gap:.5rem}.stalux-stats-site-stats{gap:.5rem;font-size:.9rem}.stalux-nav-text{white-space:nowrap;color:var(--white-full)}.stalux-badge-group-items{padding-top:.35rem}.stalux-u-text-center{text-align:center}.stalux-badge-group-hint{color:var(--white-70p);font-size:.7rem}.stalux-badge-group-title{color:var(--white-80p)}.stalux-weather-heading{font-size:inherit;font-weight:inherit;margin:0}nav{width:100%;min-width:0;max-width:100%}html{scroll-padding-top:var(--stalux-nav-offset,var(--stalux-site-header-height))}.stalux-site-header{z-index:990;width:100%;min-height:var(--stalux-site-header-min-height);isolation:isolate;justify-content:center;align-items:center;display:flex;position:sticky;top:0}.stalux-nav-scroll-btn[data-visible]{pointer-events:auto;opacity:1}.stalux-nav-scroll-btn[data-side=left]{left:4px}.stalux-nav-scroll-btn[data-side=right]{right:4px}.stalux-nav-scroll-btn svg{stroke:currentColor;stroke-width:2px;fill:none;width:20px;height:20px}.stalux-nav-link,.stalux-nav-link:visited{cursor:pointer;width:100%;color:inherit;min-height:44px;font:inherit;background:0 0;border:none;align-items:center;gap:.5rem;padding:.5rem .75rem;text-decoration:none;display:flex}.stalux-nav-link .nav-icon{stroke:var(--white-full)}.stalux-nav-toggle svg{width:32px;height:32px}.stalux-nav-overlay[data-state=show]{pointer-events:auto;opacity:1;display:block}#stalux-to-archive-archs{margin:2em 0 0}#stalux-date-date-time{text-shadow:var(--stalux-text-shadow);color:var(--white-full);flex-direction:column;align-items:center;margin:2em 0 0;display:flex}#stalux-date-date-time #stalux-date-date{min-height:1.25em;margin-bottom:.5em;font-size:2em;line-height:1.25}#stalux-date-date-time #stalux-date-time{font-variant-numeric:tabular-nums;min-height:1.25em;font-size:3em;line-height:1.25}.stalux-theme-info span,.stalux-copyright span{margin:0}.stalux-theme-info a{color:#fffc;margin:0 .2em;text-decoration:none;transition:color .3s}.stalux-beian-beian-info a{color:#ffffffb3;font-size:.9rem;text-decoration:none;transition:color .3s}.stalux-beian-public-security-beian img{width:15px;height:15px;display:inline-block}.stalux-badge-group-summary::-webkit-details-marker,.stalux-badge-group[open] .stalux-badge-group-hint[data-hint=collapsed],.stalux-badge-group:not([open]) .stalux-badge-group-hint[data-hint=expanded]{display:none}.stalux-badge-group[open] .stalux-badge-group-arrow{transform:rotate(90deg)}@media (hover:hover) and (pointer:fine){.stalux-nav-scroll-btn:hover{background:0 0}.stalux-nav-container:hover .stalux-nav-scroll-btn[data-visible]{opacity:1}.stalux-nav-link:hover .nav-icon,.stalux-nav-link:hover .stalux-nav-text{stroke:var(--stalux-accent-color);color:var(--stalux-accent-color)}.stalux-theme-info a:hover{color:#fff;text-decoration:none}.stalux-beian-beian-info a:hover,.stalux-beian-public-security-beian:hover{color:#ffffffe6;text-decoration:none}.stalux-badge-group-summary:hover{color:var(--white-70p)}}@media (width>=601px){.stalux-site-nav{isolation:isolate;border-radius:0 0 16px 16px;transition:background-color .18s}.stalux-site-nav[data-scrolled]{backdrop-filter:blur(20px)saturate(140%);box-shadow:0 1px 0 var(--white-10p), 0 8px 24px #0000001f;background:#181818c7}}@media (width<=1100px){.stalux-nav-list{gap:1rem}.stalux-nav-item{margin-left:.6em;margin-right:.6em;font-size:1em}}@media (width<=900px){.stalux-nav-item{margin-left:.25em;margin-right:.25em;font-size:.95em}.stalux-nav-list{gap:.5rem}}@media (width<=768px){.stalux-footer{padding:1.2rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}.stalux-badges{gap:.3rem}.stalux-badge{width:auto}}@media (width<=600px){html{--stalux-site-header-min-height:4rem}.stalux-nav-toggle[data-state=open]{visibility:hidden;pointer-events:none}.stalux-nav-toggle[data-scrolled]{backdrop-filter:blur(20px)saturate(140%);box-shadow:0 1px 0 var(--white-10p), 0 8px 24px #0000001f;background:#181818c7;border-radius:12px}.stalux-nav-toggle{display:block;position:fixed;top:.75rem;left:.75rem}.stalux-nav-container{overflow:visible}nav{width:auto}.stalux-nav-list{pointer-events:none;z-index:1000;background:var(--black-20p);backdrop-filter:blur(20px);flex-direction:column;justify-content:flex-start;align-items:flex-start;gap:0;width:min(220px,100vw - 1rem);height:100vh;padding:.5rem 1rem 2rem;position:fixed;top:0;left:0;right:auto;overflow-y:auto;transform:translate(-100%)}.stalux-nav-list[data-state=open]{pointer-events:auto;transform:translate(0)}.stalux-nav-item{border-bottom:1px solid var(--white-10p);flex-shrink:0;width:100%;margin:0;font-size:.9em}.stalux-nav-text{display:inline}.stalux-nav-link{width:100%;min-height:44px;font-size:inherit;padding:.45rem .75rem}.stalux-nav-link .nav-icon{width:18px;height:18px}}@media (width<=480px){.stalux-footer{padding:1rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}}.stalux-main,.stalux-post-page,.stalux-post-layout>*,.stalux-link-card-content,.stalux-word-list-item,.stalux-word-card{box-sizing:border-box;min-width:0;max-width:100%}.stalux-main{overflow-wrap:anywhere}.stalux-link-shell{min-width:0;max-width:100%;display:block}.stalux-link-card{box-sizing:border-box;width:100%;max-width:100%}.stalux-category-shell{width:100%;min-width:0}.stalux-author-figure>a{flex-shrink:0;display:inline-block}.stalux-author-figure img{display:block}.stalux-post-card-shell,.stalux-category-shell,.stalux-link-shell{position:relative}.stalux-post-card,.stalux-category-card,.stalux-link-card,.stalux-word-card{transition:transform .2s,background-color .2s,border-color .2s,box-shadow .2s}:is(.stalux-post-card-shell,.stalux-category-shell):focus-within>*,.stalux-link-shell:focus-visible>*{box-shadow:var(--card-box-shadow-hover)}:is(button,summary,[role=button]):focus-visible{outline:2px solid var(--stalux-accent-color);outline-offset:3px}@media (prefers-reduced-motion:reduce){.stalux-main *,.stalux-main :before,.stalux-main :after{transition-duration:0s!important;animation:none!important}.stalux-post-card,.stalux-category-card,.stalux-link-card,.stalux-link-card-content,.stalux-card-post-cover img,.stalux-category-icon,.stalux-author-figure img,.stalux-link-card-avatar img{transform:none!important}}@keyframes stalux-social-stalux-social-arrive{0%{opacity:0;transform:translateY(.75rem)rotate(-12deg)}to{opacity:1;transform:translateY(0)rotate(0)}}@keyframes stalux-random-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}}@layer utilities{.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visible{visibility:visible}.absolute{position:absolute}.static{position:static}.grid{display:grid}.block{display:block}.inline-block{display:inline-block}.hidden{display:none}.h-full{height:100%}.flex{display:flex}.flex-shrink{flex-shrink:1}.flex-wrap{flex-wrap:wrap}.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))}.b,.border{border-width:1px}.text-inherit{color:inherit}.no-underline{text-decoration:none}.outline{outline-style:solid}}@layer theme;@layer base{html,body{color:#fff;width:100%;height:100%;margin:0;& main{flex:1}scrollbar-color:var(--stalux-scrollbar-thumb) var(--stalux-scrollbar-track);scrollbar-width:auto;font-family:var(--font-body), "Noto Sans SC", "Noto Sans CJK SC", system-ui, -apple-system, sans-serif;text-rendering:optimizelegibility;line-height:1.25;display:block}body{height:auto;min-height:100%}html{scrollbar-gutter:stable;background-color:var(--stalux-bg-color,#202020)}html[data-clarity-id]{opacity:1!important}a{color:inherit;cursor:pointer;text-decoration-skip-ink:auto;background-color:#0000;text-decoration:none;transition:color .2s,text-shadow .2s;position:relative}.a-none{cursor:pointer;text-decoration-skip-ink:auto;background-color:#0000;color:inherit!important;text-decoration:none!important}.a-none:after{display:none!important}a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):after{content:"";background:linear-gradient(90deg, var(--stalux-accent-color), var(--accent-50p));transform-origin:0;pointer-events:none;width:100%;height:1px;transition:transform .25s cubic-bezier(.25,.9,.2,1);position:absolute;bottom:-2px;left:0;transform:scaleX(0)}a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):focus-visible:after,a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):active:after{transform:scaleX(1)}@media (hover:hover) and (pointer:fine){a:not(.a-none):not([data-stalux-card]):not(.stalux-nav-link):hover:after{transform:scaleX(1)}}a:active,a:focus-visible{text-decoration:none}@media (hover:hover) and (pointer:fine){a:hover{text-decoration:none}}a:focus{outline:none}a:focus-visible{outline:2px solid var(--stalux-accent-color);outline-offset:2px}ul{margin:0;padding:0;list-style:none}.none{display:none}.stalux-root{color:#fff;min-height:100vh}.stalux-main{min-height:calc(100vh - var(--stalux-site-header-height,3rem))}:root{--stalux-bg-color:#202020;--stalux-bg-filter:none;--stalux-scrollbar-thumb:#7d7d7d;--stalux-scrollbar-thumb-hover:#9a9a9a;--stalux-scrollbar-track:#2a2a2a;--stalux-site-header-min-height:3.75rem;--stalux-site-header-height:var(--stalux-site-header-min-height);--stalux-text-shadow:var(--stalux-accent-color) .08rem .08rem .18rem;--stalux-accent-color:#eab308;--stalux-accent-rgb:234 179 8;--surface-glass:var(--black-05p);--surface-glass-shadow:var(--shadow-sidebar);--card-bg:var(--surface-glass);--card-box-shadow:0 10px 24px #00000014;--card-box-shadow-hover:0 14px 28px #0000001f;--tag-bg:#ffffff1f;--tag-bg-hover:#ffffff2e;--category-bg:#ffffff14;--category-bg-hover:#ffffff24;--date-bg:#42a0ff2e;--date-border:#42a0ff40;--updated-bg:#ffc94733;--updated-border:#ffc9474d;--white-full:#fff;--white-95p:#fffffff2;--white-90p:#ffffffe6;--white-85p:#ffffffd9;--white-80p:#ffffffe6;--white-78p:#ffffffc7;--white-75p:#ffffffbf;--white-70p:#ffffffdb;--white-60p:#fff9;--white-50p:#ffffff80;--white-30p:#ffffff4d;--white-20p:#fff3;--white-15p:#ffffff26;--white-10p:#ffffff1a;--white-05p:#ffffff0d;--black-30p:#0000004d;--black-20p:#0003;--black-15p:#00000026;--black-10p:#0000001a;--black-05p:#0000000d;--accent-90p:rgb(var(--stalux-accent-rgb) / .9);--accent-85p:rgb(var(--stalux-accent-rgb) / .85);--accent-80p:rgb(var(--stalux-accent-rgb) / .8);--accent-70p:rgb(var(--stalux-accent-rgb) / .7);--accent-60p:rgb(var(--stalux-accent-rgb) / .6);--accent-50p:rgb(var(--stalux-accent-rgb) / .5);--accent-40p:rgb(var(--stalux-accent-rgb) / .4);--accent-30p:rgb(var(--stalux-accent-rgb) / .3);--accent-20p:rgb(var(--stalux-accent-rgb) / .2);--transition-fast:.18s ease;--transition-medium:.2s ease;--transition-normal:.25s ease;--transition-slow:.3s ease;--space-2xs:.25rem;--space-xs:.75rem;--space-sm:.5rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--space-3xl:4rem;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-full:9999px;--shadow-sm:0 2px 5px #0000001a;--shadow-md:0 6px 16px #0003;--shadow-lg:0 8px 20px #0000004d;--shadow-xl:0 12px 26px #00000047;--shadow-sidebar:0 16px 20px -12px #000000d6, 10px 4px 16px -12px #00000094, -10px 4px 16px -12px #00000094;--shadow-dark:#000000c0;--muted-text:#bbb;--font-body:"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;--font-size-subtitle:1.35rem;--font-size-base:1.1rem;--font-size-small:.95rem;--font-size-smaller:.9rem;--font-size-tiny:.85rem;--border-thin:1px;--border-medium:2px;--border-thick:3px}@supports not (scrollbar-color:auto){::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--stalux-scrollbar-track);border-radius:4px}::-webkit-scrollbar-thumb{background:var(--stalux-scrollbar-thumb);border-radius:4px;transition:background .3s ease-out}@media (hover:hover) and (pointer:fine){::-webkit-scrollbar-thumb:hover{background:var(--stalux-scrollbar-thumb-hover)}}::-webkit-scrollbar-corner{background:var(--stalux-scrollbar-track)}}.bg-layer{z-index:-10;background-position:50%;background-repeat:repeat;background-size:auto;background-color:var(--stalux-bg-color,transparent);filter:var(--stalux-bg-filter,none);pointer-events:none;opacity:0;forced-color-adjust:none;color-scheme:light only;transition:opacity .2s ease-out;position:fixed;inset:0}@media (prefers-reduced-motion:reduce){.bg-layer{transition-duration:0s}}footer{padding:var(--space-lg) var(--space-md);text-align:center;color:var(--muted-text);border-top:1px solid var(--white-10p);backdrop-filter:blur(16px);background:linear-gradient(#0c0e122e,#0c0e12b8)}}@media print{[data-stalux-offscreen-code],[data-stalux-offscreen-svg]{content-visibility:visible!important;contain-intrinsic-block-size:none!important}}[data-stalux-home-content]{flex-direction:column;align-items:center;width:100%;display:flex}[data-stalux-scroll-sentinel]{visibility:hidden;pointer-events:none;width:1px;height:1px;position:absolute;top:240px;left:0}@media (prefers-reduced-motion:reduce){.stalux-main,.stalux-main *,.postLayout *{scroll-behavior:auto!important;transition-duration:1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important}}
3
3
  /*$vite$:1*/
@@ -30,6 +30,17 @@
30
30
  --waline-accent-rgb: var(--stalux-accent-rgb);
31
31
  }
32
32
 
33
+ /* Waline ships its own light palette. Pin the text tokens to the theme scale so
34
+ comment text, timestamps and the credit line stay above WCAG AA (4.5:1) on the
35
+ dark canvas, and keep accent-coloured text on the build-time lightened accent. */
36
+ .waline-comment-container {
37
+ --waline-theme-color: var(--accent-text);
38
+ --waline-color: var(--white-90p);
39
+ --waline-info-color: var(--white-50p);
40
+ --waline-light-grey: var(--white-70p);
41
+ --waline-dark-grey: var(--white-70p);
42
+ }
43
+
33
44
  /* 评论区容器与博客风格一致 */
34
45
  .waline-comment-container {
35
46
  margin-top: 3rem;
@@ -153,18 +164,13 @@
153
164
 
154
165
  /* 修改用户名样式 */
155
166
  .wl-nick {
156
- color: rgb(var(--waline-accent-rgb) / 0.95) !important;
167
+ color: var(--waline-theme-color) !important;
157
168
  font-weight: 600;
158
169
  transition:
159
170
  background-color 180ms ease,
160
171
  border-color 180ms ease,
161
172
  color 180ms ease;
162
173
  }
163
- @media (hover: hover) and (pointer: fine) {
164
- .wl-nick:hover {
165
- color: rgb(var(--waline-accent-rgb) / 1) !important;
166
- }
167
- }
168
174
 
169
175
  /* 评论内容样式 */
170
176
  .wl-content {
@@ -312,7 +318,7 @@
312
318
  }
313
319
 
314
320
  .wl-sort li.active {
315
- color: rgb(var(--waline-accent-rgb) / 1);
321
+ color: var(--waline-theme-color);
316
322
  font-weight: 500;
317
323
  }
318
324
 
@@ -356,7 +362,7 @@
356
362
  }
357
363
 
358
364
  .wl-power a {
359
- color: rgb(var(--waline-accent-rgb) / 0.9);
365
+ color: var(--waline-theme-color);
360
366
  text-decoration: none;
361
367
  transition:
362
368
  background-color 180ms ease,
@@ -365,7 +371,6 @@
365
371
  }
366
372
  @media (hover: hover) and (pointer: fine) {
367
373
  .wl-power a:hover {
368
- color: rgb(var(--waline-accent-rgb) / 1);
369
374
  text-decoration: underline;
370
375
  }
371
376
  }
@@ -392,13 +397,6 @@
392
397
  outline: none;
393
398
  }
394
399
 
395
- .waline-comment-container *::-moz-selection {
396
- background: rgb(var(--waline-accent-rgb) / 0.25);
397
- color: inherit;
398
- border: none;
399
- outline: none;
400
- }
401
-
402
400
  /* 响应式调整 */
403
401
  @media (max-width: 768px) {
404
402
  .waline-comment-container {
@@ -85,7 +85,9 @@ mover > :first-child {
85
85
 
86
86
  /* ===== Firefox fixes ===== */
87
87
 
88
- @-moz-document url-prefix() {
88
+ /* `-moz-appearance` only resolves in Gecko, so this replaces the deprecated
89
+ `@-moz-document url-prefix()` gate with feature detection. */
90
+ @supports (-moz-appearance: none) {
89
91
  /* Prevent flex-wrap */
90
92
  math {
91
93
  display: inline;
@@ -1,14 +1,4 @@
1
- @keyframes spin {
2
- from {
3
- transform: rotate(0deg);
4
- }
5
- to {
6
- transform: rotate(360deg);
7
- }
8
- }
9
-
10
- /* Swup animates one bounded introductory region through route-motion.ts.
11
- Long documents stay visible; the shell is never part of an outgoing layer. */
1
+ /* Page layout only: the router swaps containers without any route animation. */
12
2
  [data-stalux-home-content] {
13
3
  display: flex;
14
4
  flex-direction: column;
@@ -61,14 +51,3 @@
61
51
  }
62
52
  }
63
53
  }
64
-
65
- @layer components {
66
- @keyframes stalux-link-avatar-spin {
67
- from {
68
- transform: rotate(0deg);
69
- }
70
- to {
71
- transform: rotate(360deg);
72
- }
73
- }
74
- }
@@ -4,6 +4,57 @@ const ACCENT_COLOR_PATTERN = /^#[\da-fA-F]{6}$/u;
4
4
 
5
5
  const ACCENT_OPACITIES = [90, 85, 80, 70, 60, 50, 40, 30, 20] as const;
6
6
 
7
+ /** Page canvas the accent text has to stay legible on (see `--stalux-bg-color`). */
8
+ const CANVAS_LUMINANCE = relativeLuminance([32, 32, 32]);
9
+
10
+ /** WCAG AA for normal text is 4.5:1; keep a small margin for translucent surfaces. */
11
+ const MIN_TEXT_CONTRAST = 4.6;
12
+
13
+ function linearChannel(value: number): number {
14
+ const channel = value / 255;
15
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
16
+ }
17
+
18
+ function relativeLuminance([red, green, blue]: readonly number[]): number {
19
+ return (
20
+ 0.2126 * linearChannel(red ?? 0) +
21
+ 0.7152 * linearChannel(green ?? 0) +
22
+ 0.0722 * linearChannel(blue ?? 0)
23
+ );
24
+ }
25
+
26
+ function contrastOnCanvas(color: readonly number[]): number {
27
+ const luminance = relativeLuminance(color);
28
+ const [lighter, darker] =
29
+ luminance > CANVAS_LUMINANCE
30
+ ? [luminance, CANVAS_LUMINANCE]
31
+ : [CANVAS_LUMINANCE, luminance];
32
+ return (lighter + 0.05) / (darker + 0.05);
33
+ }
34
+
35
+ function toHex([red, green, blue]: readonly number[]): string {
36
+ return `#${[red, green, blue]
37
+ .map((value) => (value ?? 0).toString(16).padStart(2, "0"))
38
+ .join("")}`;
39
+ }
40
+
41
+ /**
42
+ * Lighten the accent toward white until it clears WCAG AA as body text.
43
+ * Accents that already pass keep their exact value, so default themes are unchanged.
44
+ */
45
+ export function accentTextColor(accentColor: string): string {
46
+ const hex = accentColor.slice(1);
47
+ const channels = [0, 2, 4].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
48
+ if (contrastOnCanvas(channels) >= MIN_TEXT_CONTRAST) return accentColor.toLowerCase();
49
+
50
+ for (let step = 1; step <= 20; step++) {
51
+ const mix = step / 20;
52
+ const lifted = channels.map((value) => Math.round(value + (255 - value) * mix));
53
+ if (contrastOnCanvas(lifted) >= MIN_TEXT_CONTRAST) return toHex(lifted);
54
+ }
55
+ return "#ffffff";
56
+ }
57
+
7
58
  /** Build the CSS custom properties shared by Stalux and its comment styles. */
8
59
  export function getAccentColorStyle(accentColor = "#EAB308"): string {
9
60
  if (!ACCENT_COLOR_PATTERN.test(accentColor)) {
@@ -19,6 +70,7 @@ export function getAccentColorStyle(accentColor = "#EAB308"): string {
19
70
  return [
20
71
  `--stalux-accent-color: ${accentColor.toLowerCase()}`,
21
72
  `--stalux-accent-rgb: ${rgb}`,
73
+ `--accent-text: ${accentTextColor(accentColor)}`,
22
74
  ...ACCENT_OPACITIES.map(
23
75
  (opacity) =>
24
76
  `${productionStyleName(`--accent-${opacity}p`)}: rgb(${red} ${green} ${blue} / ${opacity / 100})`,
@@ -103,7 +103,7 @@ function pageUrl(site: string, path: string, exportMd: boolean): string {
103
103
 
104
104
  /** 生成文章链接,可切换 .md 源码版本 */
105
105
  export function postUrl(site: string, abbrlink: string | number, exportMd: boolean): string {
106
- return exportMd && abbrlink ? `${site}/posts/${abbrlink}.md` : `${site}/posts/${abbrlink}/`;
106
+ return exportMd && abbrlink ? `${site}/posts/${abbrlink}.md` : `${site}/posts/${abbrlink}`;
107
107
  }
108
108
 
109
109
  /** 获取已发布文章并按日期降序排列 */
package/src/utils/feed.ts CHANGED
@@ -70,7 +70,7 @@ export async function buildFeedItems(
70
70
  title: post.data.title,
71
71
  pubDate: new Date(toMachineDateTime(post.data.date, stalux.timezone)),
72
72
  description: cached?.desc || post.data.desc || "",
73
- link: `/posts/${post.data.abbrlink}/`,
73
+ link: `/posts/${post.data.abbrlink}`,
74
74
  categories: post.data.categories || [],
75
75
  customData,
76
76
  };
@@ -45,10 +45,20 @@ export function getMarkdownPath(pathname: string): string | undefined {
45
45
  return undefined;
46
46
  }
47
47
 
48
+ /**
49
+ * The public form of an HTML route. Every page route is emitted without a
50
+ * trailing slash (`/posts/x`, `/tags/x`, `/archives`); only the root keeps its
51
+ * single slash. Real files — Markdown exports, JSON/XML endpoints, images —
52
+ * are returned untouched so they keep the extension that identifies them.
53
+ */
54
+ export function toRoutePath(pathname: string): string {
55
+ const path = normalizePublicPath(pathname);
56
+ return path === "/" ? "/" : path;
57
+ }
58
+
48
59
  export function toPublicUrl(site: string, pathname: string, markdown = false): string {
49
60
  const base = site.replace(/\/+$/u, "");
50
- const normalized = normalizePublicPath(pathname);
51
- const path = markdown ? getMarkdownPath(normalized) : normalized;
52
- if (!path) return `${base}${normalized === "/" ? "/" : `${normalized}/`}`;
53
- return markdown ? `${base}${path}` : `${base}${path === "/" ? "/" : `${path}/`}`;
61
+ const path = markdown ? getMarkdownPath(pathname) : toRoutePath(pathname);
62
+ if (!path) return `${base}${toRoutePath(pathname)}`;
63
+ return `${base}${path}`;
54
64
  }
@@ -59,8 +59,9 @@ export function buildSeoDocument(input: SeoDocumentInput): SeoDocument {
59
59
  const routeKind = getRouteKind(pathname);
60
60
  const siteUrl = normalizeSiteUrl(site.canonical ?? site.url);
61
61
  const normalizedPath = normalizePublicPath(pathname);
62
- const canonicalPath = normalizedPath === "/" ? "/" : `${normalizedPath}/`;
63
- const canonical = new URL(canonicalPath, `${siteUrl}/`).href;
62
+ // Canonical mirrors the URL the router actually serves: page routes carry no
63
+ // trailing slash, so /posts/x and /archives are canonical, /posts/x/ is not.
64
+ const canonical = new URL(normalizedPath, `${siteUrl}/`).href;
64
65
  const title = routeKind === "home" && site.seoTitle ? site.seoTitle : page.title;
65
66
  const siteName = site.title;
66
67
  const siteTitleSuffix = ` | ${site.title}`;
@@ -1,340 +0,0 @@
1
- import type { RouteMotionPreset } from "../utils/route-motion-policy";
2
-
3
- export interface RouteMotionEvent {
4
- // Retained for integrations using the shared gates with a native transition.
5
- // Stalux's Swup router leaves this unset and owns the animation lifecycle.
6
- viewTransition?: {
7
- ready?: PromiseLike<unknown>;
8
- finished?: PromiseLike<unknown>;
9
- };
10
- signal?: AbortSignal;
11
- }
12
-
13
- export interface RouteMotionOptions {
14
- skip?: boolean;
15
- /** Animate the route's own content stage instead of a bounded heading area. */
16
- content?: boolean;
17
- preset?: RouteMotionPreset;
18
- browserMotion?: boolean;
19
- /** Releases the outgoing fill effect inside the entry cue's sync block. */
20
- releaseOutgoing?: () => void;
21
- }
22
-
23
- export interface RouteMotion {
24
- ready: Promise<void>;
25
- finished: Promise<void>;
26
- start: (heading: HTMLElement | null, options?: RouteMotionOptions) => void;
27
- cancel: () => void;
28
- }
29
-
30
- interface HeadingMotion {
31
- opacity: number;
32
- duration: number;
33
- x?: number;
34
- y?: number;
35
- }
36
-
37
- // Legacy heading cues remain available to integrations. The router requests
38
- // one content animation; it never stacks a second animation on the heading.
39
- const headingMotions: Record<Exclude<RouteMotionPreset, "none">, HeadingMotion> = {
40
- glide: { opacity: 0.86, duration: 50 },
41
- article: { opacity: 0.86, duration: 50 },
42
- enter: { opacity: 0.86, duration: 70, y: 6 },
43
- up: { opacity: 0.9, duration: 60, y: -4 },
44
- taxonomy: { opacity: 0.85, duration: 60 },
45
- previous: { opacity: 0.9, duration: 70, x: -8 },
46
- next: { opacity: 0.9, duration: 70, x: 8 },
47
- panel: { opacity: 0.9, duration: 60, y: 4 },
48
- home: { opacity: 0.9, duration: 60, y: 4 },
49
- restore: { opacity: 0.96, duration: 40 },
50
- error: { opacity: 0.9, duration: 40 },
51
- };
52
-
53
- const MAIN = "main[data-stalux-route]";
54
- const ARTICLE = "article.stalux-article";
55
- // Authored per page type by the theme, not inferred from layout: the article
56
- // body, archive list, tag cloud, post list, word list, link list, home block.
57
- const CONTENT = "[data-stalux-motion-content]";
58
- // The one node a page nominates as its cue stage. Preferred over measuring,
59
- // because measuring an article column forces a full layout of a document that
60
- // is tens of thousands of pixels tall, before it has ever been painted.
61
- const STAGE = "[data-stalux-motion-stage]";
62
- // A persist node marks the three-column article shell. Those sidebars keep
63
- // their identity across visits and must never travel with a route cue.
64
- const PERSIST = "[data-swup-persist]";
65
-
66
- interface StageMotion {
67
- opacity: number;
68
- /**
69
- * Enter offset in px. Positive y enters from below, positive x from the right.
70
- * Zero means a pure cross-fade: the column is replaced in place.
71
- */
72
- distance: number;
73
- axis: "x" | "y";
74
- duration: number;
75
- }
76
-
77
- // One entry per route kind, because the components are not interchangeable: an
78
- // article opens from below and going back up to an index pulls away upward.
79
- //
80
- // Article-to-article is the exception: the reader keeps the same shell (sidebars,
81
- // header) and only the column changes, so the column fades in place instead of
82
- // travelling. Sliding a 28,000px column sideways made the whole page look like it
83
- // jerked, and the shell already tells the reader where they are.
84
- const stageMotions: Record<Exclude<RouteMotionPreset, "none">, StageMotion> = {
85
- previous: { opacity: 0, distance: 0, axis: "x", duration: 150 },
86
- next: { opacity: 0, distance: 0, axis: "x", duration: 150 },
87
- // Entering a different article without a reading-direction trigger.
88
- article: { opacity: 0, distance: 0, axis: "y", duration: 150 },
89
- // Reaching an article from any list or the home block.
90
- enter: { opacity: 0, distance: 56, axis: "y", duration: 170 },
91
- home: { opacity: 0, distance: 48, axis: "y", duration: 160 },
92
- // Leaving an article for the archive or a taxonomy index.
93
- up: { opacity: 0, distance: -44, axis: "y", duration: 150 },
94
- // Moving between two taxonomy detail pages.
95
- taxonomy: { opacity: 0, distance: 36, axis: "y", duration: 140 },
96
- // About / links / words: single-panel pages.
97
- panel: { opacity: 0, distance: 44, axis: "y", duration: 150 },
98
- glide: { opacity: 0, distance: 40, axis: "y", duration: 140 },
99
- // History traversal restores what the reader already saw.
100
- restore: { opacity: 0, distance: -36, axis: "y", duration: 140 },
101
- error: { opacity: 0, distance: 28, axis: "y", duration: 130 },
102
- };
103
-
104
- const laidOut = (element: HTMLElement): boolean => {
105
- const rect = element.getBoundingClientRect();
106
- return rect.width > 0 && rect.height > 0;
107
- };
108
-
109
- /**
110
- * The single node a route cue may animate for the current document.
111
- *
112
- * A page can nominate its stage; only when it does not is the largest authored
113
- * content stage measured. Measuring is the fallback, not the rule: on an article
114
- * the column is tens of thousands of pixels tall and measuring it forces a full
115
- * layout of the incoming document at the exact moment the entry cue needs to
116
- * start. Sidebars (persisted shells), the header and the footer never qualify.
117
- */
118
- export function getRouteStage(): HTMLElement | null {
119
- const main = document.querySelector<HTMLElement>(MAIN);
120
- if (!main?.isConnected) return null;
121
- const authored = main.querySelector<HTMLElement>(STAGE);
122
- if (authored?.isConnected && !authored.closest(PERSIST)) return authored;
123
- const candidates: HTMLElement[] = [];
124
- const article = main.querySelector<HTMLElement>(ARTICLE);
125
- if (article) candidates.push(article);
126
- for (const element of main.querySelectorAll<HTMLElement>(CONTENT)) candidates.push(element);
127
-
128
- let stage: HTMLElement | null = null;
129
- let area = 0;
130
- for (const element of candidates) {
131
- // A stage inside a persisted subtree is the shell itself.
132
- if (!element.isConnected || element.closest(PERSIST)) continue;
133
- const rect = element.getBoundingClientRect();
134
- if (rect.width <= 0 || rect.height <= 0) continue;
135
- const current = rect.width * rect.height;
136
- if (current > area) {
137
- area = current;
138
- stage = element;
139
- }
140
- }
141
- if (stage) return stage;
142
- if (main.querySelector(PERSIST)) return null;
143
- return laidOut(main) ? main : null;
144
- }
145
-
146
- /** Offset applied to the outgoing document, continuing the entry direction. */
147
- export function getStageExit(preset: RouteMotionPreset): StageMotion | null {
148
- if (preset === "none") return null;
149
- const motion = stageMotions[preset];
150
- // Entering from below means leaving upward, so both halves read as one
151
- // continuous flow instead of an enter/leave bounce.
152
- return { ...motion, distance: -Math.round(motion.distance * 0.72) };
153
- }
154
-
155
- const motions = new WeakMap<object, RouteMotion>();
156
- const settle = (promise?: PromiseLike<unknown>): Promise<void> =>
157
- Promise.resolve(promise).then(
158
- () => undefined,
159
- () => undefined,
160
- );
161
- function completion() {
162
- let resolve = () => {};
163
- const promise = new Promise<void>((done) => {
164
- resolve = done;
165
- });
166
- return { promise, resolve };
167
- }
168
-
169
- /** A route transition never captures a long article, list, or off-screen heading. */
170
- export function getRouteMotionTarget(focus: HTMLElement | null): HTMLElement | null {
171
- if (!(focus?.isConnected && focus.hasAttribute("data-stalux-motion-focus"))) return null;
172
- const visible = (element: HTMLElement, maxHeight: number) => {
173
- const rect = element.getBoundingClientRect();
174
- return (
175
- rect.width > 0 &&
176
- rect.height > 0 &&
177
- rect.height <= maxHeight &&
178
- rect.top < window.innerHeight &&
179
- rect.bottom > 0 &&
180
- rect.left < window.innerWidth &&
181
- rect.right > 0
182
- );
183
- };
184
- if (!visible(focus, Math.min(320, window.innerHeight / 2))) return null;
185
- const region = focus.closest?.<HTMLElement>("[data-stalux-motion-region]");
186
- return region?.isConnected && visible(region, Math.min(420, window.innerHeight * 0.6))
187
- ? region
188
- : focus;
189
- }
190
-
191
- const regionMotions: Record<Exclude<RouteMotionPreset, "none">, HeadingMotion> = {
192
- glide: { opacity: 0.72, duration: 80 },
193
- article: { opacity: 0.72, duration: 90, y: 4 },
194
- enter: { opacity: 0.72, duration: 90, y: 6 },
195
- up: { opacity: 0.8, duration: 80, y: -4 },
196
- taxonomy: { opacity: 0.72, duration: 80, x: 6 },
197
- previous: { opacity: 0.76, duration: 90, x: -8 },
198
- next: { opacity: 0.76, duration: 90, x: 8 },
199
- panel: { opacity: 0.76, duration: 80, y: 4 },
200
- home: { opacity: 0.76, duration: 90, y: 4 },
201
- restore: { opacity: 1, duration: 0 },
202
- error: { opacity: 0.85, duration: 50 },
203
- };
204
-
205
- /** Share Swup's actual content animation with background and analytics consumers. */
206
- export function prepareRouteMotion(event: RouteMotionEvent): RouteMotion {
207
- const native = event.viewTransition;
208
- const key = native ?? event;
209
- const existing = motions.get(key);
210
- if (existing) return existing;
211
-
212
- // Installation is a separate gate: before-swap consumers must not mistake
213
- // the gap before Swup's incoming animation hook for a completed transition.
214
- const installed = completion();
215
- const animationDone = completion();
216
- const canceled = completion();
217
- let animation: Animation | undefined;
218
- let started = false;
219
- let disposed = false;
220
- const clearAnimation = () => {
221
- const active = animation;
222
- animation = undefined;
223
- try {
224
- // Remove fill effects and restore the authored final styles. No
225
- // inline transform/opacity survives completion, failure or abort.
226
- active?.cancel();
227
- } catch {
228
- // Releasing an already detached effect must not hold up navigation.
229
- }
230
- animationDone.resolve();
231
- };
232
- const cancel = () => {
233
- if (disposed) return;
234
- disposed = true;
235
- started = true;
236
- clearAnimation();
237
- installed.resolve();
238
- canceled.resolve();
239
- event.signal?.removeEventListener("abort", cancel);
240
- };
241
- const ready = Promise.race([
242
- canceled.promise,
243
- Promise.all([settle(native?.ready), installed.promise]).then(() => undefined),
244
- ]);
245
- const finished = Promise.race([
246
- canceled.promise,
247
- Promise.all([settle(native?.finished), animationDone.promise]).then(() => undefined),
248
- ]);
249
- const motion: RouteMotion = {
250
- ready,
251
- finished,
252
- start(heading, options) {
253
- if (started) return;
254
- started = true;
255
- // Every exit path must release the outgoing fill, or the swapped
256
- // document stays transparent behind a cancelled cue.
257
- const release = options?.releaseOutgoing;
258
- try {
259
- const preset = options?.preset ?? "glide";
260
- const mobile = window.matchMedia?.("(max-width: 768px)").matches;
261
- if (
262
- options?.skip ||
263
- options?.browserMotion ||
264
- preset === "none" ||
265
- (options?.content && preset === "restore") ||
266
- // History never replays an entry on a phone: the reader is
267
- // returning to a position, not arriving somewhere new.
268
- (mobile && preset === "restore") ||
269
- document.hidden ||
270
- window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
271
- ) {
272
- release?.();
273
- animationDone.resolve();
274
- return;
275
- }
276
- const stage = options?.content ? getRouteStage() : null;
277
- const target = stage ?? getRouteMotionTarget(heading);
278
- if (!target || typeof target.animate !== "function") {
279
- release?.();
280
- animationDone.resolve();
281
- return;
282
- }
283
- // Cancelling the outgoing fill and starting the entry cue must
284
- // share one synchronous block. Split across frames, the reader
285
- // sees the new document at full opacity for a frame first.
286
- release?.();
287
- if (stage) {
288
- const spec = stageMotions[preset];
289
- // Phones get a shorter throw; the same offset reads as a much
290
- // larger move on a narrow viewport.
291
- const distance = mobile ? Math.round(spec.distance * 0.6) : spec.distance;
292
- const from =
293
- spec.axis === "x"
294
- ? `translate3d(${distance}px, 0, 0)`
295
- : `translate3d(0, ${distance}px, 0)`;
296
- // A zero distance is a cross-fade in place. Authoring a
297
- // translate3d(0,0,0) would put a 28,000px column on its own
298
- // compositor layer for nothing.
299
- const keyframes: PropertyIndexedKeyframes = { opacity: [spec.opacity, 1] };
300
- if (distance !== 0) keyframes.transform = [from, "translate3d(0, 0, 0)"];
301
- animation = stage.animate(keyframes, {
302
- duration: mobile ? Math.round(spec.duration * 0.9) : spec.duration,
303
- // Strong ease-out: quick arrival, soft settle. The
304
- // built-in ease-out is too weak to read at this size.
305
- easing: "cubic-bezier(0.23, 1, 0.32, 1)",
306
- fill: "both",
307
- });
308
- } else {
309
- const spec = options?.content ? regionMotions[preset] : headingMotions[preset];
310
- const keyframes: PropertyIndexedKeyframes = { opacity: [spec.opacity, 1] };
311
- // Phones use a pure fade. Explicit previous/next alone imply a
312
- // lateral reading direction; history never replays the entry.
313
- if (!mobile && (spec.x || spec.y)) {
314
- keyframes.transform = [
315
- `translate3d(${spec.x ?? 0}px, ${spec.y ?? 0}px, 0)`,
316
- "translate3d(0px, 0px, 0)",
317
- ];
318
- }
319
- animation = target.animate(keyframes, {
320
- duration: mobile ? (options?.content ? 60 : 40) : spec.duration,
321
- easing: "cubic-bezier(0.2, 0.7, 0.2, 1)",
322
- fill: "both",
323
- });
324
- }
325
- void animation.finished.then(clearAnimation, clearAnimation);
326
- } catch {
327
- release?.();
328
- clearAnimation();
329
- } finally {
330
- installed.resolve();
331
- }
332
- },
333
- cancel,
334
- };
335
- motions.set(key, motion);
336
- if (event.signal?.aborted) cancel();
337
- else event.signal?.addEventListener("abort", cancel, { once: true });
338
- void finished.then(() => event.signal?.removeEventListener("abort", cancel));
339
- return motion;
340
- }