@xingwangzhe/stalux 1.35.11 → 1.35.13

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 (43) hide show
  1. package/README.md +24 -11
  2. package/package.json +5 -2
  3. package/src/components/stalux/common/Typewriter.astro +32 -1
  4. package/src/components/stalux/posts/postContent.astro +7 -1
  5. package/src/index.ts +51 -104
  6. package/src/internal/bounded-tasks.ts +28 -0
  7. package/src/internal/compact-styles.ts +55 -13
  8. package/src/internal/font-slices.ts +150 -47
  9. package/src/internal/html-images.ts +2 -0
  10. package/src/internal/html-output.ts +98 -0
  11. package/src/internal/image-dimensions.ts +99 -74
  12. package/src/internal/page-output-cache.ts +26 -9
  13. package/src/internal/public-assets.ts +59 -0
  14. package/src/internal/runtime-cache-key.ts +14 -3
  15. package/src/internal/style-names.generated.ts +27 -26
  16. package/src/layouts/PostLayout.astro +1 -0
  17. package/src/layouts/Stalux.astro +3 -1
  18. package/src/pages/api/post.abbrlink.json.ts +0 -3
  19. package/src/pages/api/posts.json.ts +16 -19
  20. package/src/pages/openapi.json.ts +3 -44
  21. package/src/pages/posts/[post].md.ts +4 -40
  22. package/src/plugins/feature-flags.ts +8 -1
  23. package/src/scripts/navigation-font.ts +28 -0
  24. package/src/scripts/navigation.ts +4 -2
  25. package/src/scripts/page-runtime.ts +20 -6
  26. package/src/scripts/pagefind-api.ts +63 -0
  27. package/src/scripts/photoswipe.ts +13 -3
  28. package/src/scripts/random-posts.ts +12 -10
  29. package/src/scripts/search-dialog.ts +2 -1
  30. package/src/scripts/view-transitions.ts +12 -1
  31. package/src/scripts/webmcp.ts +397 -141
  32. package/src/scripts/word-list.ts +13 -3
  33. package/src/styles/base.css +7 -0
  34. package/src/styles/generated.css +1 -1
  35. package/src/utils/collections-stats.ts +9 -22
  36. package/src/utils/content-index.ts +13 -4
  37. package/src/utils/dayjs.ts +26 -11
  38. package/src/utils/feed.ts +30 -29
  39. package/src/utils/post-lookup.ts +51 -0
  40. package/src/utils/post-markdown.ts +31 -0
  41. package/src/utils/site-path.ts +4 -0
  42. package/src/utils/toc-scrollspy.ts +20 -6
  43. package/src/utils/word-count-utils.ts +45 -41
@@ -14,10 +14,16 @@ function scrollToWord(hash: string): void {
14
14
  registerPageLifecycle("word-list", () => {
15
15
  if (!document.querySelector("[data-word-hash]")) return;
16
16
  const controller = new AbortController();
17
+ let firstFrame = 0;
18
+ let secondFrame = 0;
17
19
 
18
20
  if (location.hash.startsWith("#word-")) {
19
- requestAnimationFrame(() => {
20
- requestAnimationFrame(() => scrollToWord(location.hash));
21
+ firstFrame = requestAnimationFrame(() => {
22
+ firstFrame = 0;
23
+ secondFrame = requestAnimationFrame(() => {
24
+ secondFrame = 0;
25
+ scrollToWord(location.hash);
26
+ });
21
27
  });
22
28
  }
23
29
 
@@ -35,5 +41,9 @@ registerPageLifecycle("word-list", () => {
35
41
  { signal: controller.signal },
36
42
  );
37
43
 
38
- return () => controller.abort();
44
+ return () => {
45
+ controller.abort();
46
+ if (firstFrame) cancelAnimationFrame(firstFrame);
47
+ if (secondFrame) cancelAnimationFrame(secondFrame);
48
+ };
39
49
  });
@@ -15,6 +15,13 @@
15
15
  var(--font-body), "Noto Sans SC", "Noto Sans CJK SC", system-ui, -apple-system,
16
16
  sans-serif;
17
17
  text-rendering: optimizeLegibility;
18
+ /* Keep line boxes stable while the page font replaces the system fallback. */
19
+ line-height: 1.25;
20
+ }
21
+
22
+ /* Reserve the classic scrollbar before content height changes across routes. */
23
+ html {
24
+ scrollbar-gutter: stable;
18
25
  }
19
26
 
20
27
  html[data-clarity-id] {
@@ -1,3 +1,3 @@
1
1
  @layer theme, base, components, utilities;
2
- @layer components{.stalux-404-home-btn{background:var(--black-20p);border:var(--border-thin) solid var(--white-20p);transition:background-color var(--transition-normal), transform var(--transition-normal), box-shadow var(--transition-normal);border-radius:var(--radius-md);padding:var(--space-md) var(--space-lg);color:var(--white-90p);font-size:var(--font-size-base);text-decoration:none;display:inline-block}.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-post-categories,.stalux-card-post-tags{gap:.4rem}.stalux-card-post-category{background:var(--category-bg);color:#fff;border-radius:6px;padding:.18rem .5rem;font-size:.84rem;line-height:1;display:inline-block}.stalux-card-post-description{overflow-wrap:anywhere;color:var(--white-70p);line-height:1.6}.stalux-card-post-meta{gap:.6rem}.stalux-card-post-tag{background:var(--tag-bg);color:#fff;border-radius:9999px;padding:.22rem .7rem;font-size:.85rem;line-height:1;display:inline-block}.stalux-card-post-title{overflow-wrap:anywhere;margin:0;line-height:1.35}.stalux-card-read-more{align-self:flex-start;margin-top:.6rem}.stalux-card-time{gap:.45rem;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-home-navs{order:-1;max-width:100%}.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:1001;cursor:pointer;min-width:48px;min-height:48px;color:inherit;background:0 0;border:none;padding:.75rem;display:none}.stalux-post-card{box-sizing:border-box;cursor:pointer;gap:var(--space-lg);border-radius:var(--radius-lg);background-color:var(--card-bg);height:100%;padding:var(--space-lg);box-shadow:var(--card-box-shadow);transition:transform .22s cubic-bezier(.22,.72,.22,1)}.stalux-stats-site-build-time{color:#fffc;margin-top:.5rem;font-size:.9rem}.stalux-stats-stats-separator{color:#ffffff80}.stalux-theme-info{color:var(--white-70p);gap:.3em;font-size:.85rem}.stalux-home-body{min-height:100vh;padding-bottom:var(--space-3xl);position:relative}.stalux-nav-container{box-sizing:border-box;max-width:100%;position:relative;overflow:hidden}.stalux-author{margin-top:4em}.stalux-card-post-cover{border-radius:var(--radius-md);flex-shrink:0;height:180px;margin-bottom:.5rem;overflow:hidden}.stalux-home-title{color:var(--white-full);text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:4em}.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-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-typewriter{min-height:2em;text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:2.5em}.stalux-404-error-container{min-height:60vh;padding:2rem}.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-beian-beian-info{gap:1rem}.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-home-show-post-list{cursor:pointer;text-shadow:var(--stalux-text-shadow);margin-top:3em;font-size:1.5em}.stalux-post-list-container{width:100%;height:auto;margin:5px 0}.stalux-post-list-container ul{flex-wrap:wrap;gap:2rem;margin:3em;padding:0;list-style-type:none;display:flex}.stalux-post-list-container ul>li{flex:min(22rem,100%);min-width:0;margin:0}.stalux-card-post-title a{color:inherit;text-decoration:none}.stalux-post-card>a:first-child{width:100%;min-width:0;display:block}.stalux-card-post-cover img{object-fit:cover;width:100%;max-width:100%;height:100%;transition:transform var(--transition-fast)}.stalux-card-post-date,.stalux-card-post-updated{color:#e8f2ff;border-radius:10px;align-items:center;gap:.35rem;width:fit-content;padding:.28rem .6rem;font-size:.9rem;line-height:1.1;display:inline-flex}.stalux-card-post-date{background:var(--date-bg);border:1px solid var(--date-border);color:#e8f2ff}.stalux-card-post-updated{background:var(--updated-bg);border:1px solid var(--updated-border);color:#fff7de}.stalux-card-post-tags a,.stalux-card-post-categories a{text-decoration:none}.stalux-card-post-tags a:focus-visible .stalux-card-post-tag{background:var(--tag-bg-hover)}.stalux-card-post-categories a:focus-visible .stalux-card-post-category{background:var(--category-bg-hover)}.stalux-social{margin-top:3em}.stalux-social ul{flex-wrap:wrap;justify-content:center;gap:1em;margin:0;padding:0;display:flex}.stalux-social ul li{box-sizing:border-box;text-align:center;background-color:var(--white-15p);border-radius:9999px;justify-content:center;align-items:center;width:2.5em;height:2.5em;font-size:large;display:inline-flex;overflow:hidden}.stalux-social ul a,.stalux-social ul .stalux-social-icon-container,.stalux-social ul .stalux-social-custom-icon{justify-content:center;align-items:center;width:100%;height:100%;line-height:1;display:inline-flex}.stalux-social[data-stalux-social-animate=true] li{animation:.28s cubic-bezier(.2,.8,.2,1) both stalux-social-stalux-social-arrive}.stalux-social[data-stalux-social-animate=true] li:nth-child(2){animation-delay:30ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(3){animation-delay:60ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(4){animation-delay:90ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(5){animation-delay:.12s}.stalux-social[data-stalux-social-animate=true] li:nth-child(6){animation-delay:.15s}.stalux-social[data-stalux-social-animate=true] li:nth-child(n+7){animation-delay:.18s}nav{width:100%;min-width:0;max-width:100%}.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-top:2em}#stalux-date-date-time{text-shadow:var(--stalux-text-shadow);color:var(--white-full);flex-direction:column;align-items:center;margin-top:2em;display:flex}#stalux-date-date-time #stalux-date-date{margin-bottom:.5em;font-size:2em}#stalux-date-date-time #stalux-date-time{font-size:3em}.stalux-author-figure{justify-content:center;align-items:center;display:flex}.stalux-author-figure img{box-shadow:0 0 10px var(--stalux-accent-color);border-radius:9999px;transition:transform .65s ease-in-out}.stalux-author-figure figcaption{text-shadow:var(--stalux-text-shadow);color:var(--white-full);margin-left:1em;font-size:2.5em;font-weight:700}.stalux-author-figure-column figcaption{text-align:center;margin-top:.5em;margin-left:0}.agent-home-summary{clip-path:inset(50%);white-space:normal;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.stalux-404-error-content h1{color:var(--white-90p);margin:0;font-size:6rem;font-weight:700}.stalux-404-error-content p{color:var(--white-70p);margin:1rem 0 2rem;font-size:1.5rem}.stalux-404-countdown-text{color:var(--white-50p)!important;margin:-.5rem 0 1.5rem!important;font-size:.95rem!important}.agent-recovery{max-width:42rem;line-height:1.7;margin:0 auto 1.5rem!important;font-size:1rem!important}.agent-recovery a{color:inherit;text-decoration-line:underline}.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-card-post-tags a:hover .stalux-card-post-tag{background:var(--tag-bg-hover)}.stalux-card-post-categories a:hover .stalux-card-post-category{background:var(--category-bg-hover)}.stalux-post-card-shell:hover .stalux-post-card .stalux-card-post-cover img{transform:scale(1.025)}.stalux-post-card-shell:hover .stalux-post-card{box-shadow:var(--card-box-shadow-hover)}.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-author-figure>a:hover img{animation:.65s ease-in-out stalux-link-avatar-spin}.stalux-404-home-btn:hover{background:var(--accent-20p);box-shadow:0 4px 12px var(--shadow-dark)}.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<=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-home-body{box-sizing:border-box;width:100%;max-width:95vw;height:auto;padding:2rem .5rem 1.5rem}.stalux-home-title{word-break:break-all;margin-top:1em;font-size:2em}.stalux-home-navs{flex-direction:column;align-items:center;gap:.5em}.stalux-home-navs nav{width:100%}#stalux-home-show-post-list{margin-top:2em;font-size:1.1em}.stalux-post-list-container ul{margin:1rem}.stalux-footer{padding:1.2rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}.stalux-author{box-sizing:border-box;width:100%;max-width:95vw;margin-top:2em;padding:1em .5em}.stalux-author-figure{gap:.5em;flex-direction:column!important}.stalux-author-figure img{max-width:20vw;max-height:20vw;width:64px!important;height:64px!important}.stalux-author-figure figcaption{text-align:center;word-break:break-all;overflow-wrap:break-word;white-space:normal;max-width:90vw;margin-top:.5em;margin-left:0;font-size:1.5em}.stalux-404-error-content h1{font-size:4rem}.stalux-404-error-content p{font-size:1.2rem}.stalux-badges{gap:.3rem}.stalux-badge{width:auto}}@media (width<=600px){.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}}@media (prefers-reduced-motion:reduce){.stalux-social[data-stalux-social-animate=true] li{animation:none}}.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-post-content-reveal{0%{opacity:0}to{opacity:1}}@keyframes stalux-link-avatar-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes stalux-word-word-glow{0%{box-shadow:0 0 30px var(--accent-50p), 0 0 80px var(--accent-30p), var(--shadow-lg);transform:scale(1.02)}to{box-shadow:0 0 20px var(--accent-40p), 0 0 60px var(--accent-20p), var(--shadow-lg);transform:scale(1)}}}@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}.px{padding-left:1rem;padding-right:1rem}.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:#4a4a4a #2a2a2a;font-family:var(--font-body), "Noto Sans SC", "Noto Sans CJK SC", system-ui, -apple-system, sans-serif;text-rendering:optimizelegibility;display:block}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:100vh}:root{--stalux-bg-color:#202020;--stalux-bg-filter:none;--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}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#2a2a2a;border-radius:4px}::-webkit-scrollbar-thumb{background:#4a4a4a;border-radius:4px;transition:background .3s ease-out}@media (hover:hover) and (pointer:fine){::-webkit-scrollbar-thumb:hover{background:#5a5a5a}}::-webkit-scrollbar-corner{background:#2a2a2a}.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 .65s ease-in-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)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.stagger-children>:first-child{animation-delay:.1s}.stagger-children>:nth-child(2){animation-delay:.15s}.stagger-children>:nth-child(3){animation-delay:.2s}.stagger-children>:nth-child(4){animation-delay:.25s}.stagger-children>:nth-child(5){animation-delay:.3s}.stagger-children>:nth-child(n+6){animation-delay:.35s}@keyframes stalux-page-exit{to{opacity:0}}@keyframes stalux-page-enter{0%{opacity:0;transform:translateY(9px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-page-enter-back{0%{opacity:0;transform:translateY(-9px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-taxonomy-enter{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-taxonomy-enter-back{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-article-enter{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-article-enter-back{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-sidebar-left-in{0%{opacity:0;transform:translate(-14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-left-out{to{opacity:0;transform:translate(-10px)}}@keyframes stalux-sidebar-left-back-in{0%{opacity:0;transform:translate(14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-left-back-out{to{opacity:0;transform:translate(10px)}}@keyframes stalux-sidebar-right-in{0%{opacity:0;transform:translate(14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-right-out{to{opacity:0;transform:translate(10px)}}@keyframes stalux-sidebar-right-back-in{0%{opacity:0;transform:translate(-14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-right-back-out{to{opacity:0;transform:translate(-10px)}}::view-transition-group(stalux-page){z-index:1}::view-transition-old(stalux-page),::view-transition-new(stalux-page){transform-origin:top}::view-transition-group(*){animation-duration:.24s;animation-timing-function:cubic-bezier(.22,.72,.22,1)}@media (prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation-duration:1ms!important;animation-delay:0s!important}.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-404-home-btn{background:var(--black-20p);border:var(--border-thin) solid var(--white-20p);transition:background-color var(--transition-normal), transform var(--transition-normal), box-shadow var(--transition-normal);border-radius:var(--radius-md);padding:var(--space-md) var(--space-lg);color:var(--white-90p);font-size:var(--font-size-base);text-decoration:none;display:inline-block}.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-post-categories,.stalux-card-post-tags{gap:.4rem}.stalux-card-post-category{background:var(--category-bg);color:#fff;border-radius:6px;padding:.18rem .5rem;font-size:.84rem;line-height:1;display:inline-block}.stalux-card-post-description{overflow-wrap:anywhere;color:var(--white-70p);line-height:1.6}.stalux-card-post-meta{gap:.6rem}.stalux-card-post-tag{background:var(--tag-bg);color:#fff;border-radius:9999px;padding:.22rem .7rem;font-size:.85rem;line-height:1;display:inline-block}.stalux-card-post-title{overflow-wrap:anywhere;margin:0;line-height:1.35}.stalux-card-read-more{align-self:flex-start;margin-top:.6rem}.stalux-card-time{gap:.45rem;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-home-navs{order:-1;max-width:100%}.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:1001;cursor:pointer;min-width:48px;min-height:48px;color:inherit;background:0 0;border:none;padding:.75rem;display:none}.stalux-post-card{box-sizing:border-box;cursor:pointer;gap:var(--space-lg);border-radius:var(--radius-lg);background-color:var(--card-bg);height:100%;padding:var(--space-lg);box-shadow:var(--card-box-shadow);transition:transform .22s cubic-bezier(.22,.72,.22,1)}.stalux-stats-site-build-time{color:#fffc;margin-top:.5rem;font-size:.9rem}.stalux-stats-stats-separator{color:#ffffff80}.stalux-theme-info{color:var(--white-70p);gap:.3em;font-size:.85rem}.stalux-home-body{min-height:100vh;padding-bottom:var(--space-3xl);position:relative}.stalux-nav-container{box-sizing:border-box;max-width:100%;position:relative;overflow:hidden}.stalux-author{margin-top:4em}.stalux-card-post-cover{border-radius:var(--radius-md);flex-shrink:0;height:180px;margin-bottom:.5rem;overflow:hidden}.stalux-home-title{color:var(--white-full);text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:4em}.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-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-typewriter{min-height:2em;text-shadow:var(--stalux-text-shadow);margin-top:1.5em;font-size:2.5em}.stalux-404-error-container{min-height:60vh;padding:2rem}.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-beian-beian-info{gap:1rem}.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-home-show-post-list{cursor:pointer;text-shadow:var(--stalux-text-shadow);margin-top:3em;font-size:1.5em}.stalux-post-list-container{width:100%;height:auto;margin:5px 0}.stalux-post-list-container ul{flex-wrap:wrap;gap:2rem;margin:3em;padding:0;list-style-type:none;display:flex}.stalux-post-list-container ul>li{flex:min(22rem,100%);min-width:0;margin:0}.stalux-card-post-title a{color:inherit;text-decoration:none}.stalux-post-card>a:first-child{width:100%;min-width:0;display:block}.stalux-card-post-cover img{object-fit:cover;width:100%;max-width:100%;height:100%;transition:transform var(--transition-fast)}.stalux-card-post-date,.stalux-card-post-updated{color:#e8f2ff;border-radius:10px;align-items:center;gap:.35rem;width:fit-content;padding:.28rem .6rem;font-size:.9rem;line-height:1.1;display:inline-flex}.stalux-card-post-date{background:var(--date-bg);border:1px solid var(--date-border);color:#e8f2ff}.stalux-card-post-updated{background:var(--updated-bg);border:1px solid var(--updated-border);color:#fff7de}.stalux-card-post-tags a,.stalux-card-post-categories a{text-decoration:none}.stalux-card-post-tags a:focus-visible .stalux-card-post-tag{background:var(--tag-bg-hover)}.stalux-card-post-categories a:focus-visible .stalux-card-post-category{background:var(--category-bg-hover)}.stalux-social{margin-top:3em}.stalux-social ul{flex-wrap:wrap;justify-content:center;gap:1em;margin:0;padding:0;display:flex}.stalux-social ul li{box-sizing:border-box;text-align:center;background-color:var(--white-15p);border-radius:9999px;justify-content:center;align-items:center;width:2.5em;height:2.5em;font-size:large;display:inline-flex;overflow:hidden}.stalux-social ul a,.stalux-social ul .stalux-social-icon-container,.stalux-social ul .stalux-social-custom-icon{justify-content:center;align-items:center;width:100%;height:100%;line-height:1;display:inline-flex}.stalux-social[data-stalux-social-animate=true] li{animation:.28s cubic-bezier(.2,.8,.2,1) both stalux-social-stalux-social-arrive}.stalux-social[data-stalux-social-animate=true] li:nth-child(2){animation-delay:30ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(3){animation-delay:60ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(4){animation-delay:90ms}.stalux-social[data-stalux-social-animate=true] li:nth-child(5){animation-delay:.12s}.stalux-social[data-stalux-social-animate=true] li:nth-child(6){animation-delay:.15s}.stalux-social[data-stalux-social-animate=true] li:nth-child(n+7){animation-delay:.18s}nav{width:100%;min-width:0;max-width:100%}.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-top:2em}#stalux-date-date-time{text-shadow:var(--stalux-text-shadow);color:var(--white-full);flex-direction:column;align-items:center;margin-top:2em;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-author-figure{justify-content:center;align-items:center;display:flex}.stalux-author-figure img{box-shadow:0 0 10px var(--stalux-accent-color);border-radius:9999px;transition:transform .65s ease-in-out}.stalux-author-figure figcaption{text-shadow:var(--stalux-text-shadow);color:var(--white-full);margin-left:1em;font-size:2.5em;font-weight:700}.stalux-author-figure-column figcaption{text-align:center;margin-top:.5em;margin-left:0}.agent-home-summary{clip-path:inset(50%);white-space:normal;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.stalux-404-error-content h1{color:var(--white-90p);margin:0;font-size:6rem;font-weight:700}.stalux-404-error-content p{color:var(--white-70p);margin:1rem 0 2rem;font-size:1.5rem}.stalux-404-countdown-text{color:var(--white-50p)!important;margin:-.5rem 0 1.5rem!important;font-size:.95rem!important}.agent-recovery{max-width:42rem;line-height:1.7;margin:0 auto 1.5rem!important;font-size:1rem!important}.agent-recovery a{color:inherit;text-decoration-line:underline}.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-card-post-tags a:hover .stalux-card-post-tag{background:var(--tag-bg-hover)}.stalux-card-post-categories a:hover .stalux-card-post-category{background:var(--category-bg-hover)}.stalux-post-card-shell:hover .stalux-post-card .stalux-card-post-cover img{transform:scale(1.025)}.stalux-post-card-shell:hover .stalux-post-card{box-shadow:var(--card-box-shadow-hover)}.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-author-figure>a:hover img{animation:.65s ease-in-out stalux-link-avatar-spin}.stalux-404-home-btn:hover{background:var(--accent-20p);box-shadow:0 4px 12px var(--shadow-dark)}.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<=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-home-body{box-sizing:border-box;width:100%;max-width:95vw;height:auto;padding:2rem .5rem 1.5rem}.stalux-home-title{word-break:break-all;margin-top:1em;font-size:2em}.stalux-home-navs{flex-direction:column;align-items:center;gap:.5em}.stalux-home-navs nav{width:100%}#stalux-home-show-post-list{margin-top:2em;font-size:1.1em}.stalux-post-list-container ul{margin:1rem}.stalux-footer{padding:1.2rem 0;font-size:.8rem}.stalux-footer-content{gap:.6rem;padding:0 .8rem}.stalux-author{box-sizing:border-box;width:100%;max-width:95vw;margin-top:2em;padding:1em .5em}.stalux-author-figure{gap:.5em;flex-direction:column!important}.stalux-author-figure img{max-width:20vw;max-height:20vw;width:64px!important;height:64px!important}.stalux-author-figure figcaption{text-align:center;word-break:break-all;overflow-wrap:break-word;white-space:normal;max-width:90vw;margin-top:.5em;margin-left:0;font-size:1.5em}.stalux-404-error-content h1{font-size:4rem}.stalux-404-error-content p{font-size:1.2rem}.stalux-badges{gap:.3rem}.stalux-badge{width:auto}}@media (width<=600px){.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}}@media (prefers-reduced-motion:reduce){.stalux-social[data-stalux-social-animate=true] li{animation:none}}.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-post-content-reveal{0%{opacity:0}to{opacity:1}}@keyframes stalux-link-avatar-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes stalux-word-word-glow{0%{box-shadow:0 0 30px var(--accent-50p), 0 0 80px var(--accent-30p), var(--shadow-lg);transform:scale(1.02)}to{box-shadow:0 0 20px var(--accent-40p), 0 0 60px var(--accent-20p), var(--shadow-lg);transform:scale(1)}}}@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}.px{padding-left:1rem;padding-right:1rem}.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:#4a4a4a #2a2a2a;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}html{scrollbar-gutter:stable}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:100vh}:root{--stalux-bg-color:#202020;--stalux-bg-filter:none;--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}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#2a2a2a;border-radius:4px}::-webkit-scrollbar-thumb{background:#4a4a4a;border-radius:4px;transition:background .3s ease-out}@media (hover:hover) and (pointer:fine){::-webkit-scrollbar-thumb:hover{background:#5a5a5a}}::-webkit-scrollbar-corner{background:#2a2a2a}.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 .65s ease-in-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)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.stagger-children>:first-child{animation-delay:.1s}.stagger-children>:nth-child(2){animation-delay:.15s}.stagger-children>:nth-child(3){animation-delay:.2s}.stagger-children>:nth-child(4){animation-delay:.25s}.stagger-children>:nth-child(5){animation-delay:.3s}.stagger-children>:nth-child(n+6){animation-delay:.35s}@keyframes stalux-page-exit{to{opacity:0}}@keyframes stalux-page-enter{0%{opacity:0;transform:translateY(9px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-page-enter-back{0%{opacity:0;transform:translateY(-9px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-taxonomy-enter{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-taxonomy-enter-back{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-article-enter{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-article-enter-back{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@keyframes stalux-sidebar-left-in{0%{opacity:0;transform:translate(-14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-left-out{to{opacity:0;transform:translate(-10px)}}@keyframes stalux-sidebar-left-back-in{0%{opacity:0;transform:translate(14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-left-back-out{to{opacity:0;transform:translate(10px)}}@keyframes stalux-sidebar-right-in{0%{opacity:0;transform:translate(14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-right-out{to{opacity:0;transform:translate(10px)}}@keyframes stalux-sidebar-right-back-in{0%{opacity:0;transform:translate(-14px)}to{opacity:1;transform:translate(0)}}@keyframes stalux-sidebar-right-back-out{to{opacity:0;transform:translate(-10px)}}::view-transition-group(stalux-page){z-index:1}::view-transition-old(stalux-page),::view-transition-new(stalux-page){transform-origin:top}::view-transition-group(*){animation-duration:.24s;animation-timing-function:cubic-bezier(.22,.72,.22,1)}@media (prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation-duration:1ms!important;animation-delay:0s!important}.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*/
@@ -1,26 +1,15 @@
1
1
  import type { AstroRuntimeLogger } from "astro";
2
2
  import { getPostContentIndex, type PostEntry } from "./content-index";
3
3
 
4
- /**
5
- * 统计 posts 中每个分类键出现的次数。
6
- * @param posts 文章集合
7
- * @param keyFn 从单篇文章取出若干分类键(如标签、分类)
8
- * @param defaultValue 文章没有任何分类键时的兜底分类名(可选)
9
- */
10
- function countBy(
11
- posts: PostEntry[],
12
- keyFn: (post: PostEntry) => string[] | undefined,
13
- defaultValue?: string,
14
- ): Array<{ name: string; count: number }> {
15
- const counts = new Map<string, number>();
4
+ /** Count the existing uncategorized fallback without building a second taxonomy map. */
5
+ function countUncategorized(posts: PostEntry[]): number {
6
+ let count = 0;
16
7
  for (const post of posts) {
17
- const keys = keyFn(post) ?? [];
18
- const names = keys.length > 0 ? keys : defaultValue ? [defaultValue] : [];
19
- for (const name of names) {
20
- counts.set(name, (counts.get(name) ?? 0) + 1);
21
- }
8
+ const names = post.data.categories ?? [];
9
+ if (!names.length) count++;
10
+ else for (const name of names) if (name === "uncategorized") count++;
22
11
  }
23
- return [...counts].map(([name, count]) => ({ name, count }));
12
+ return count;
24
13
  }
25
14
 
26
15
  /**
@@ -42,9 +31,7 @@ export async function getCategoryCountList(logger?: AstroRuntimeLogger) {
42
31
  name,
43
32
  count: categoryPosts.length,
44
33
  }));
45
- const uncategorized = countBy(posts, (post) => post.data.categories, "uncategorized").find(
46
- ({ name }) => name === "uncategorized",
47
- );
48
- if (uncategorized) counts.push(uncategorized);
34
+ const uncategorized = countUncategorized(posts);
35
+ if (uncategorized) counts.push({ name: "uncategorized", count: uncategorized });
49
36
  return counts;
50
37
  }
@@ -47,8 +47,8 @@ function addToTaxonomy<T extends IndexablePost>(
47
47
  map: Map<string, MutableTaxonomyBucket<T>>,
48
48
  post: T,
49
49
  names: string[] | undefined,
50
+ timestamp: number,
50
51
  ): void {
51
- const timestamp = toTimestamp(post.data.updated ?? post.data.date);
52
52
  for (const name of new Set(names ?? [])) {
53
53
  const existing = map.get(name);
54
54
  if (existing) {
@@ -94,8 +94,16 @@ function finalizeTaxonomy<T extends IndexablePost>(
94
94
  export function buildPostContentIndex<T extends IndexablePost>(
95
95
  sourcePosts: readonly T[],
96
96
  ): PostContentIndex<T> {
97
+ const times = new Map<T, { date: number; updated: number }>();
98
+ for (const post of sourcePosts) {
99
+ const date = toTimestamp(post.data.date);
100
+ times.set(post, {
101
+ date,
102
+ updated: post.data.updated == null ? date : toTimestamp(post.data.updated),
103
+ });
104
+ }
97
105
  const posts = sourcePosts.toSorted(
98
- (left, right) => toTimestamp(right.data.date) - toTimestamp(left.data.date),
106
+ (left, right) => (times.get(right)?.date ?? 0) - (times.get(left)?.date ?? 0),
99
107
  );
100
108
  const byAbbrlink = new Map<string, T>();
101
109
  const tags = new Map<string, MutableTaxonomyBucket<T>>();
@@ -108,8 +116,9 @@ export function buildPostContentIndex<T extends IndexablePost>(
108
116
  throw new Error(`Duplicate post abbrlink: ${abbrlink}`);
109
117
  }
110
118
  byAbbrlink.set(abbrlink, post);
111
- addToTaxonomy(tags, post, post.data.tags);
112
- addToTaxonomy(categories, post, post.data.categories);
119
+ const timestamp = times.get(post)?.updated ?? 0;
120
+ addToTaxonomy(tags, post, post.data.tags, timestamp);
121
+ addToTaxonomy(categories, post, post.data.categories, timestamp);
113
122
  neighbors.set(abbrlink, {
114
123
  previous: posts[index + 1],
115
124
  next: index > 0 ? posts[index - 1] : undefined,
@@ -18,6 +18,31 @@ export const toTimestamp = (v: unknown): number => parseDate(v).getTime();
18
18
 
19
19
  const pad2 = (n: number): string => String(n).padStart(2, "0");
20
20
 
21
+ // Bounded across user-supplied zones; invalid zones still throw before caching.
22
+ const timezoneFormatters = new Map<string, Intl.DateTimeFormat>();
23
+ function getTimezoneFormatter(timeZone: string): Intl.DateTimeFormat {
24
+ let formatter = timezoneFormatters.get(timeZone);
25
+ if (!formatter) {
26
+ formatter = new Intl.DateTimeFormat("en-US", {
27
+ timeZone,
28
+ year: "numeric",
29
+ month: "2-digit",
30
+ day: "2-digit",
31
+ hour: "2-digit",
32
+ minute: "2-digit",
33
+ second: "2-digit",
34
+ hourCycle: "h23",
35
+ timeZoneName: "longOffset",
36
+ });
37
+ if (timezoneFormatters.size >= 16) {
38
+ const oldest = timezoneFormatters.keys().next().value;
39
+ if (oldest !== undefined) timezoneFormatters.delete(oldest);
40
+ }
41
+ timezoneFormatters.set(timeZone, formatter);
42
+ }
43
+ return formatter;
44
+ }
45
+
21
46
  /**
22
47
  * 轻量 format:仅覆盖本项目实际用到的 token。
23
48
  * 支持:yyyy MM dd HH mm ss
@@ -51,17 +76,7 @@ export function formatInTimeZone(
51
76
  const d = date instanceof Date ? date : new Date(date);
52
77
  if (Number.isNaN(d.getTime())) return "";
53
78
 
54
- const parts = new Intl.DateTimeFormat("en-US", {
55
- timeZone,
56
- year: "numeric",
57
- month: "2-digit",
58
- day: "2-digit",
59
- hour: "2-digit",
60
- minute: "2-digit",
61
- second: "2-digit",
62
- hourCycle: "h23",
63
- timeZoneName: "longOffset",
64
- }).formatToParts(d);
79
+ const parts = getTimezoneFormatter(timeZone).formatToParts(d);
65
80
 
66
81
  const get = (type: Intl.DateTimeFormatPartTypes): string =>
67
82
  parts.find((p) => p.type === type)?.value ?? "";
package/src/utils/feed.ts CHANGED
@@ -32,43 +32,44 @@ export async function buildFeedItems(
32
32
  updatedTag: string,
33
33
  logger?: AstroRuntimeLogger,
34
34
  ): Promise<FeedItem[]> {
35
+ const timestamps = new Map(
36
+ posts.map((post) => [post, toTimestamp(post.data.updated || post.data.date)]),
37
+ );
35
38
  const sortedPosts = posts.toSorted((a, b) => {
36
39
  // Newest first: compare post dates, use the greater of updated/date
37
- const aTime = toTimestamp(a.data.updated || a.data.date);
38
- const bTime = toTimestamp(b.data.updated || b.data.date);
40
+ const aTime = timestamps.get(a) ?? 0;
41
+ const bTime = timestamps.get(b) ?? 0;
39
42
  return bTime - aTime;
40
43
  });
41
44
 
42
45
  const descriptions = await getPostDescriptions(logger);
43
46
  logDetail(logger, "feed", `rendering ${sortedPosts.length} entries; format=${updatedTag}`);
44
- return Promise.all(
45
- sortedPosts.map(async (post) => {
46
- const cached = descriptions.get(String(post.data.abbrlink));
47
+ return sortedPosts.map((post) => {
48
+ const cached = descriptions.get(String(post.data.abbrlink));
47
49
 
48
- let customData = "";
49
- if (post.data.updated) {
50
- const parsed = parseDate(post.data.updated);
51
- if (isValid(parsed)) {
52
- const updatedIso = formatInTimeZone(
53
- parsed,
54
- stalux.timezone,
55
- "yyyy-MM-dd'T'HH:mm:ssXXX",
56
- );
57
- customData += `<${updatedTag}>${updatedIso}</${updatedTag}>`;
58
- }
59
- }
60
- if (post.data.cc) {
61
- customData += `<rights>${post.data.cc}</rights>`;
50
+ let customData = "";
51
+ if (post.data.updated) {
52
+ const parsed = parseDate(post.data.updated);
53
+ if (isValid(parsed)) {
54
+ const updatedIso = formatInTimeZone(
55
+ parsed,
56
+ stalux.timezone,
57
+ "yyyy-MM-dd'T'HH:mm:ssXXX",
58
+ );
59
+ customData += `<${updatedTag}>${updatedIso}</${updatedTag}>`;
62
60
  }
61
+ }
62
+ if (post.data.cc) {
63
+ customData += `<rights>${post.data.cc}</rights>`;
64
+ }
63
65
 
64
- return {
65
- title: post.data.title,
66
- pubDate: new Date(toMachineDateTime(post.data.date, stalux.timezone)),
67
- description: cached?.desc || post.data.desc || "",
68
- link: `/posts/${post.data.abbrlink}/`,
69
- categories: post.data.categories || [],
70
- customData,
71
- };
72
- }),
73
- );
66
+ return {
67
+ title: post.data.title,
68
+ pubDate: new Date(toMachineDateTime(post.data.date, stalux.timezone)),
69
+ description: cached?.desc || post.data.desc || "",
70
+ link: `/posts/${post.data.abbrlink}/`,
71
+ categories: post.data.categories || [],
72
+ customData,
73
+ };
74
+ });
74
75
  }
@@ -0,0 +1,51 @@
1
+ interface SearchablePost {
2
+ abbrlink: string;
3
+ title: string;
4
+ tags?: string[];
5
+ categories?: string[];
6
+ desc?: string;
7
+ }
8
+
9
+ /** One immutable snapshot; duplicate IDs and fuzzy matches keep first-match order. */
10
+ export function createPostLookup<T extends SearchablePost>(posts: readonly T[]) {
11
+ const byId = new Map<string, T>();
12
+ const searchable = posts.map((post) => {
13
+ if (!byId.has(post.abbrlink)) byId.set(post.abbrlink, post);
14
+ return {
15
+ post,
16
+ title: post.title.toLowerCase(),
17
+ tags: post.tags?.map((tag) => tag.toLowerCase()) ?? [],
18
+ categories: post.categories?.map((category) => category.toLowerCase()) ?? [],
19
+ description: post.desc?.toLowerCase() ?? "",
20
+ };
21
+ });
22
+ return {
23
+ findById: (id: string): T | undefined => byId.get(id),
24
+ findMatches(keyword: string, limit: number): T[] {
25
+ const query = keyword.toLowerCase();
26
+ const titles: T[] = [];
27
+ const other: T[] = [];
28
+ for (const entry of searchable) {
29
+ if (entry.title.includes(query)) {
30
+ titles.push(entry.post);
31
+ if (titles.length >= limit) break;
32
+ } else if (
33
+ other.length < limit &&
34
+ (entry.tags.some((tag) => tag.includes(query)) ||
35
+ entry.categories.some((category) => category.includes(query)) ||
36
+ entry.description.includes(query))
37
+ ) {
38
+ other.push(entry.post);
39
+ }
40
+ }
41
+ return [...titles, ...other].slice(0, limit);
42
+ },
43
+ findByKeyword(keyword: string): T | undefined {
44
+ const query = keyword.toLowerCase();
45
+ return (
46
+ searchable.find(({ title }) => title.includes(query)) ??
47
+ searchable.find(({ tags }) => tags.some((tag) => tag.includes(query)))
48
+ )?.post;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,31 @@
1
+ import { Document } from "yaml";
2
+
3
+ interface MarkdownPostData {
4
+ title: string;
5
+ tags?: string[];
6
+ categories?: string[];
7
+ date: string;
8
+ updated?: string;
9
+ desc: string;
10
+ abbrlink: string | number;
11
+ draft?: boolean;
12
+ cc?: string;
13
+ cover?: string;
14
+ }
15
+
16
+ /** Serialize the collection's parsed metadata as YAML AST; preserve the original Markdown body. */
17
+ export function reconstructMarkdown(data: MarkdownPostData, body = ""): string {
18
+ const frontmatter = new Document({
19
+ title: data.title,
20
+ ...(data.tags?.length ? { tags: data.tags } : {}),
21
+ ...(data.categories?.length ? { categories: data.categories } : {}),
22
+ date: data.date,
23
+ ...(data.updated ? { updated: data.updated } : {}),
24
+ desc: data.desc,
25
+ abbrlink: String(data.abbrlink),
26
+ ...(data.draft ? { draft: true } : {}),
27
+ ...(data.cc && data.cc !== "CC-BY-NC-SA-4.0" ? { cc: data.cc } : {}),
28
+ ...(data.cover ? { cover: data.cover } : {}),
29
+ });
30
+ return `---\n${frontmatter.toString({ lineWidth: 0 })}---\n${body}${body.endsWith("\n") ? "" : "\n"}`;
31
+ }
@@ -0,0 +1,4 @@
1
+ /** Resolve a known static resource under Astro's configured deployment base. */
2
+ export function sitePath(pathname: string, base = import.meta.env.BASE_URL): string {
3
+ return `${(base || "/").replace(/\/+$/, "")}/${pathname.replace(/^\/+/, "")}`;
4
+ }
@@ -25,6 +25,8 @@ export function initTocScrollSpy(activeClass: string): (() => void) | undefined
25
25
  let pending = false;
26
26
  let frameId = 0;
27
27
  let sidebarHovered = false;
28
+ const headingOrder = new Map(headings.map((heading, index) => [heading, index]));
29
+ const visibleHeadings = new Set<HTMLElement>();
28
30
  const controller = new AbortController();
29
31
 
30
32
  // 鼠标悬停在侧边栏上时跳过自动滚动,避免跟用户手动操作冲突
@@ -61,7 +63,12 @@ export function initTocScrollSpy(activeClass: string): (() => void) | undefined
61
63
  };
62
64
 
63
65
  const observer = new IntersectionObserver(
64
- () => {
66
+ (entries) => {
67
+ for (const entry of entries) {
68
+ const heading = entry.target as HTMLElement;
69
+ if (entry.isIntersecting) visibleHeadings.add(heading);
70
+ else visibleHeadings.delete(heading);
71
+ }
65
72
  // 用 rAF 合并同一帧内的多次回调,避免频繁触发布局计算
66
73
  if (pending) return;
67
74
  pending = true;
@@ -70,11 +77,17 @@ export function initTocScrollSpy(activeClass: string): (() => void) | undefined
70
77
  pending = false;
71
78
 
72
79
  // 找到视口上方 35% 区域内最靠上的标题
73
- let current =
74
- headings.find((h) => {
75
- const rect = h.getBoundingClientRect();
76
- return rect.top < window.innerHeight * 0.35 && rect.bottom > 0;
77
- }) ?? null;
80
+ let current: HTMLElement | null = null;
81
+ let currentOrder = headings.length;
82
+ for (const heading of visibleHeadings) {
83
+ const order = headingOrder.get(heading) ?? headings.length;
84
+ if (order >= currentOrder) continue;
85
+ const rect = heading.getBoundingClientRect();
86
+ if (rect.top < window.innerHeight * 0.35 && rect.bottom > 0) {
87
+ current = heading;
88
+ currentOrder = order;
89
+ }
90
+ }
78
91
 
79
92
  // 兜底:滚动到文章底部时,取最后一个仍在视口上方或已经越过顶部的标题
80
93
  current ??=
@@ -111,6 +124,7 @@ export function initTocScrollSpy(activeClass: string): (() => void) | undefined
111
124
  return () => {
112
125
  controller.abort();
113
126
  observer.disconnect();
127
+ visibleHeadings.clear();
114
128
  if (frameId) cancelAnimationFrame(frameId);
115
129
  activeLink?.classList.remove(activeClass);
116
130
  };
@@ -7,65 +7,69 @@
7
7
  import { getCollection } from "astro:content";
8
8
  import { analyzeFeatureFlags } from "@plugins/feature-flags";
9
9
  import type { AstroRuntimeLogger } from "astro";
10
+ import { forEachBounded } from "../internal/bounded-tasks";
10
11
  import { logDetail, logFailure } from "./diagnostics";
11
12
 
12
13
  // ---------------------------------------------------------------------------
13
14
  // 模块级缓存(整个构建周期只计算一次)
14
15
  // ---------------------------------------------------------------------------
15
16
 
16
- let _cachedTotal: number | null = null;
17
+ interface PostStatistics {
18
+ total: number;
19
+ descriptions: Map<string, { desc: string; wordCount: number }>;
20
+ }
17
21
 
18
- /**
19
- * 获取所有已发布文章的总字数(从 body 估算)。
20
- * 结果会被缓存,同一构建周期内重复调用不会重新计算。
21
- */
22
- export async function getTotalWordCount(logger?: AstroRuntimeLogger): Promise<number> {
23
- if (_cachedTotal !== null) {
24
- logDetail(logger, "word-count", "total cache hit");
25
- return _cachedTotal;
22
+ let statisticsPromise: Promise<PostStatistics> | undefined;
23
+
24
+ function loadStatistics(logger?: AstroRuntimeLogger): Promise<PostStatistics> {
25
+ if (!statisticsPromise) {
26
+ statisticsPromise = (async () => {
27
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
28
+ const results = new Array<{ id: string; desc: string; wordCount: number }>(
29
+ posts.length,
30
+ );
31
+ await forEachBounded(
32
+ posts.map((post, index) => ({ post, index })),
33
+ 4,
34
+ async ({ post, index }) => {
35
+ results[index] = {
36
+ id: String(post.data.abbrlink ?? post.id),
37
+ desc: post.data.desc || "",
38
+ wordCount: (await analyzeFeatureFlags(post.body, logger)).wordCount,
39
+ };
40
+ },
41
+ );
42
+ const descriptions = new Map<string, { desc: string; wordCount: number }>();
43
+ let total = 0;
44
+ for (const result of results) {
45
+ total += result.wordCount;
46
+ descriptions.set(result.id, { desc: result.desc, wordCount: result.wordCount });
47
+ }
48
+ logDetail(logger, "word-count", `analyzed ${posts.length} posts`);
49
+ return { total, descriptions };
50
+ })().catch((error: unknown) => {
51
+ statisticsPromise = undefined;
52
+ throw error;
53
+ });
26
54
  }
27
- logDetail(logger, "word-count", "total cache miss");
55
+ return statisticsPromise;
56
+ }
57
+
58
+ /** Shared in-flight statistics prevent concurrent pages from repeating collection work. */
59
+ export async function getTotalWordCount(logger?: AstroRuntimeLogger): Promise<number> {
28
60
  try {
29
- const posts = await getCollection("posts", ({ data }) => !data.draft);
30
- const results = await Promise.all(
31
- posts.map((post) => analyzeFeatureFlags(post.body, logger)),
32
- );
33
- _cachedTotal = results.reduce((sum, result) => sum + result.wordCount, 0);
34
- logDetail(logger, "word-count", `analyzed ${posts.length} posts`);
35
- return _cachedTotal;
61
+ return (await loadStatistics(logger)).total;
36
62
  } catch (error) {
37
63
  logFailure(logger, "word-count", "total calculation failed; returning zero", error);
38
64
  return 0;
39
65
  }
40
66
  }
41
67
 
42
- let _cachedDescriptions: Map<string, { desc: string; wordCount: number }> | null = null;
43
-
44
- /**
45
- * 获取所有已发布文章的描述和字数缓存,供 RSS/Atom/LLMs feed 共享。
46
- * - desc :直接从 frontmatter 读取(post.data.desc)
47
- * - wordCount:从 Sätteri AST 分析 post.body
48
- */
68
+ /** Keep collection insertion order and frontmatter descriptions unchanged. */
49
69
  export async function getPostDescriptions(
50
70
  logger?: AstroRuntimeLogger,
51
71
  ): Promise<Map<string, { desc: string; wordCount: number }>> {
52
- if (_cachedDescriptions !== null) {
53
- logDetail(logger, "word-count", "description cache hit");
54
- return _cachedDescriptions;
55
- }
56
- logDetail(logger, "word-count", "description cache miss");
57
- const posts = await getCollection("posts", ({ data }) => !data.draft);
58
- const results = await Promise.all(
59
- posts.map(async (post) => ({
60
- id: String(post.data.abbrlink ?? post.id),
61
- desc: post.data.desc || "",
62
- wordCount: (await analyzeFeatureFlags(post.body, logger)).wordCount,
63
- })),
64
- );
65
- _cachedDescriptions = new Map(
66
- results.map((r) => [r.id, { desc: r.desc, wordCount: r.wordCount }]),
67
- );
68
- return _cachedDescriptions;
72
+ return (await loadStatistics(logger)).descriptions;
69
73
  }
70
74
 
71
75
  export function formatWordCount(count: number, lang?: string): string {