@xingwangzhe/stalux 1.33.0 → 1.34.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.
package/README.md CHANGED
@@ -299,3 +299,5 @@ The friend links list is up to 1600px wide with 16px horizontal padding. Cards f
299
299
  Article bodies and summary cards are up to 840px wide. Summaries use the existing post `desc` and are omitted when empty. Markdown blockquotes appear as callout cards without new configuration or syntax.
300
300
 
301
301
  Visitor weather is optional: set `weather: { enabled: true }` in `stalux/config/site.yml` to show it as plain text in the footer (with visitor IP and approximate location) and below the desktop article clock. Omitted or `false` disables it. It automatically requests an approximate city from IP.SB (with IPinfo as fallback) and weather from Open-Meteo, only while visible; no browser GPS permission is requested. These external services receive the visitor IP, and Open-Meteo receives rounded coordinates. A 20-minute session cache stores the city, visitor IP and weather, without coordinates. Visitors behind VPNs may see the exit city. Hidden tabs, offscreen cards and Astro navigation stop work; unavailable services show a retry action and retain previous weather. Open-Meteo's free endpoint is for non-commercial use subject to its limits; the card links to its attribution licence. No server endpoint or extra package is needed.
302
+
303
+ Archives keep a transparent, single-column year/month timeline with all articles expanded. Each month gently fades and moves into place once when it enters the viewport (280ms), using native browser APIs without extra dependencies. Reduced motion, printing, missing browser APIs and disabled JavaScript retain the complete static list. Mobile dates sit above wrapping article titles. No new configuration is required. Article titles retain a single hover underline; hovering or keyboard-focusing a title previews its existing `desc` on the right, falling below near viewport edges. Empty descriptions show no preview, and touch navigation stays direct. The preview uses the browser top layer (with a high stacking-level fallback) to avoid clipping or overlap.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -68,6 +68,7 @@ const archiveYears = [...archivesByYearMonth.entries()]
68
68
  {archiveYear.months.map(({ month, posts: monthPosts }) => (
69
69
  <div
70
70
  class="stalux-archive-month-section"
71
+ data-archive-month
71
72
  style={{
72
73
  "--archive-estimate": `${Math.ceil(monthPosts.length / 12) * 12 * 48 + 40}px`,
73
74
  }}
@@ -91,6 +92,7 @@ const archiveYears = [...archivesByYearMonth.entries()]
91
92
  <a
92
93
  href={`/posts/${post.data.abbrlink}/`}
93
94
  class="stalux-archive-post-link"
95
+ data-archive-description={post.data.desc || ""}
94
96
  data-astro-prefetch="viewport"
95
97
  >
96
98
  {post.data.title}
@@ -111,3 +113,19 @@ const archiveYears = [...archivesByYearMonth.entries()]
111
113
  <slot name="main" />
112
114
  </div>
113
115
  </section>
116
+ <div
117
+ id="stalux-archive-description"
118
+ class="stalux-archive-description"
119
+ data-archive-tooltip
120
+ role="tooltip"
121
+ hidden
122
+ ></div>
123
+
124
+ <script>
125
+ import { mountArchiveDescriptions } from "../../../scripts/archive-descriptions";
126
+ import { mountArchiveReveal } from "../../../scripts/archive-reveal";
127
+ import { registerPageLifecycle } from "../../../scripts/page-runtime";
128
+
129
+ registerPageLifecycle("archive-reveal", mountArchiveReveal);
130
+ registerPageLifecycle("archive-descriptions", mountArchiveDescriptions);
131
+ </script>
@@ -0,0 +1,139 @@
1
+ interface AnchorRect {
2
+ left: number;
3
+ right: number;
4
+ top: number;
5
+ bottom: number;
6
+ }
7
+
8
+ /** Prefer the title's right side, falling below it when that side has no room. */
9
+ export function getArchiveDescriptionPosition(
10
+ anchor: AnchorRect,
11
+ width: number,
12
+ height: number,
13
+ viewportWidth: number,
14
+ viewportHeight: number,
15
+ ) {
16
+ const fitsRight = anchor.right + 12 + width <= viewportWidth - 16;
17
+ const left = fitsRight ? anchor.right + 12 : anchor.left;
18
+ const top = fitsRight ? anchor.top : anchor.bottom + 8;
19
+ return {
20
+ left: Math.max(16, Math.min(left, viewportWidth - width - 16)),
21
+ top: Math.max(16, Math.min(top, viewportHeight - height - 16)),
22
+ };
23
+ }
24
+
25
+ export function mountArchiveDescriptions() {
26
+ const root = document.querySelector<HTMLElement>(".stalux-archive-container");
27
+ const tooltip = document.querySelector<HTMLElement>("[data-archive-tooltip]");
28
+ if (!root || !tooltip) return;
29
+ const usePopover =
30
+ typeof tooltip.showPopover === "function" && typeof tooltip.hidePopover === "function";
31
+ if (usePopover) tooltip.setAttribute("popover", "manual");
32
+ let popoverOpen = false;
33
+ const hover = window.matchMedia("(hover: hover) and (pointer: fine)");
34
+ let active: HTMLAnchorElement | undefined;
35
+ let previousDescription: string | null = null;
36
+ let closeTimer: ReturnType<typeof setTimeout> | undefined;
37
+ const clearClose = () => {
38
+ clearTimeout(closeTimer);
39
+ closeTimer = undefined;
40
+ };
41
+ const hide = () => {
42
+ clearClose();
43
+ if (active) {
44
+ if (previousDescription === null) active.removeAttribute("aria-describedby");
45
+ else active.setAttribute("aria-describedby", previousDescription);
46
+ }
47
+ active = undefined;
48
+ if (popoverOpen) {
49
+ tooltip.hidePopover();
50
+ popoverOpen = false;
51
+ }
52
+ tooltip.hidden = true;
53
+ tooltip.textContent = "";
54
+ };
55
+ const show = (link: HTMLAnchorElement) => {
56
+ const description = link.dataset.archiveDescription?.trim();
57
+ if (!description) return;
58
+ clearClose();
59
+ if (active !== link) {
60
+ hide();
61
+ active = link;
62
+ previousDescription = link.getAttribute("aria-describedby");
63
+ link.setAttribute(
64
+ "aria-describedby",
65
+ [previousDescription, tooltip.id].filter(Boolean).join(" "),
66
+ );
67
+ tooltip.textContent = description;
68
+ }
69
+ tooltip.hidden = false;
70
+ if (usePopover && !popoverOpen) {
71
+ tooltip.showPopover();
72
+ popoverOpen = true;
73
+ }
74
+ const { left, top } = getArchiveDescriptionPosition(
75
+ link.getBoundingClientRect(),
76
+ tooltip.offsetWidth,
77
+ tooltip.offsetHeight,
78
+ document.documentElement.clientWidth,
79
+ document.documentElement.clientHeight,
80
+ );
81
+ tooltip.style.left = `${left}px`;
82
+ tooltip.style.top = `${top}px`;
83
+ };
84
+ const linkFrom = (target: EventTarget | null) =>
85
+ target instanceof Element
86
+ ? target.closest<HTMLAnchorElement>(".stalux-archive-post-link")
87
+ : null;
88
+ const enter = (event: Event) => {
89
+ if (event.type === "pointerover" && !hover.matches) return;
90
+ const link = linkFrom(event.target);
91
+ if (link && root.contains(link)) show(link);
92
+ };
93
+ const leave = (event: Event) => {
94
+ const link = linkFrom(event.target);
95
+ if (link !== active) return;
96
+ const related = (event as FocusEvent).relatedTarget;
97
+ if (related instanceof Node && (active?.contains(related) || tooltip.contains(related)))
98
+ return;
99
+ clearClose();
100
+ closeTimer = setTimeout(hide, 120);
101
+ };
102
+ const handleEscape = (event: KeyboardEvent) => {
103
+ if (event.key === "Escape") hide();
104
+ };
105
+ const scroll = (event: Event) => {
106
+ if (event.target instanceof Node && tooltip.contains(event.target)) return;
107
+ hide();
108
+ };
109
+ const visibility = () => {
110
+ if (document.hidden) hide();
111
+ };
112
+ root.addEventListener("pointerover", enter);
113
+ root.addEventListener("pointerout", leave);
114
+ root.addEventListener("focusin", enter);
115
+ root.addEventListener("focusout", leave);
116
+ tooltip.addEventListener("pointerenter", clearClose);
117
+ tooltip.addEventListener("pointerleave", hide);
118
+ document.addEventListener("keydown", handleEscape);
119
+ document.addEventListener("scroll", scroll, { capture: true, passive: true });
120
+ document.addEventListener("visibilitychange", visibility);
121
+ window.addEventListener("resize", hide);
122
+ window.addEventListener("beforeprint", hide);
123
+ hover.addEventListener("change", hide);
124
+ return () => {
125
+ hide();
126
+ root.removeEventListener("pointerover", enter);
127
+ root.removeEventListener("pointerout", leave);
128
+ root.removeEventListener("focusin", enter);
129
+ root.removeEventListener("focusout", leave);
130
+ tooltip.removeEventListener("pointerenter", clearClose);
131
+ tooltip.removeEventListener("pointerleave", hide);
132
+ document.removeEventListener("keydown", handleEscape);
133
+ document.removeEventListener("scroll", scroll, true);
134
+ document.removeEventListener("visibilitychange", visibility);
135
+ window.removeEventListener("resize", hide);
136
+ window.removeEventListener("beforeprint", hide);
137
+ hover.removeEventListener("change", hide);
138
+ };
139
+ }
@@ -0,0 +1,76 @@
1
+ /** Enhance visible static months; never hide or collapse archive content. */
2
+ export function mountArchiveReveal() {
3
+ const months = [...document.querySelectorAll<HTMLElement>("[data-archive-month]")];
4
+ if (
5
+ !months.length ||
6
+ typeof IntersectionObserver === "undefined" ||
7
+ typeof months[0]?.animate !== "function"
8
+ )
9
+ return;
10
+
11
+ const motion = window.matchMedia("(prefers-reduced-motion: reduce)");
12
+ const print = window.matchMedia("print");
13
+ const visible = new Set<HTMLElement>();
14
+ const played = new Set<HTMLElement>();
15
+ const animations = new Map<Animation, () => void>();
16
+ let disposed = false;
17
+
18
+ const cancelAnimations = () => {
19
+ for (const [animation, finish] of animations) {
20
+ animation.removeEventListener("finish", finish);
21
+ animation.cancel();
22
+ }
23
+ animations.clear();
24
+ };
25
+ const reveal = () => {
26
+ if (disposed || document.hidden) return;
27
+ for (const month of visible) {
28
+ if (played.has(month)) continue;
29
+ played.add(month);
30
+ observer.unobserve(month);
31
+ if (motion.matches || print.matches) continue;
32
+ const animation = month.animate(
33
+ [
34
+ { opacity: 0, transform: "translateY(8px)" },
35
+ { opacity: 1, transform: "translateY(0)" },
36
+ ],
37
+ { duration: 280, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
38
+ );
39
+ const finish = () => {
40
+ animation.removeEventListener("finish", finish);
41
+ animations.delete(animation);
42
+ animation.cancel();
43
+ };
44
+ animations.set(animation, finish);
45
+ animation.addEventListener("finish", finish);
46
+ }
47
+ };
48
+ const observer = new IntersectionObserver((entries) => {
49
+ if (disposed) return;
50
+ for (const entry of entries) {
51
+ const month = entry.target as HTMLElement;
52
+ if (entry.isIntersecting) visible.add(month);
53
+ else visible.delete(month);
54
+ }
55
+ reveal();
56
+ });
57
+ const update = () => {
58
+ if (document.hidden || motion.matches || print.matches) cancelAnimations();
59
+ reveal();
60
+ };
61
+ for (const month of months) observer.observe(month);
62
+ document.addEventListener("visibilitychange", update);
63
+ window.addEventListener("beforeprint", cancelAnimations);
64
+ motion.addEventListener("change", update);
65
+ print.addEventListener("change", update);
66
+ return () => {
67
+ disposed = true;
68
+ observer.disconnect();
69
+ cancelAnimations();
70
+ visible.clear();
71
+ document.removeEventListener("visibilitychange", update);
72
+ window.removeEventListener("beforeprint", cancelAnimations);
73
+ motion.removeEventListener("change", update);
74
+ print.removeEventListener("change", update);
75
+ };
76
+ }
@@ -1,3 +1,3 @@
1
1
  @layer theme, base, components, utilities;
2
- @layer components{.stalux-archive-month-section{content-visibility:auto;contain-intrinsic-block-size:auto var(--archive-estimate,600px);margin-left:calc(-1 * var(--tl-pad));padding-left:var(--tl-pad);margin-bottom:2rem;position:relative}.stalux-archive-post-list{content-visibility:auto;contain-intrinsic-block-size:auto var(--archive-group-estimate,576px);margin:0 0 0 calc(-1 * var(--tl-pad));padding:0 0 0 var(--tl-pad);list-style-type:none}.stalux-archive-year-section{margin-bottom:var(--space-2xl);position:relative}.stalux-archive-container{max-width:1000px;padding:var(--space-xl) var(--space-md);margin:0 auto}.stalux-archives-archives-container{max-width:1000px;margin:0 auto;padding:2rem 1rem}.stalux-archive-year-count{background-color:var(--accent-20p);color:var(--white-90p);border-radius:1rem;margin-left:1rem;padding:.3rem .8rem;font-size:.9rem}.stalux-archive-year-header{align-items:center;margin-bottom:1.5rem;display:flex}.stalux-archive-no-posts{background-color:var(--black-10p);text-align:center;width:100%;color:var(--white-70p);border-radius:.5rem;padding:3rem 1rem;font-size:1.2rem}.stalux-archive-post-date{min-width:90px;color:var(--white-60p);font-size:.85rem}.stalux-archive-post-link{text-decoration:none}.stalux-archive-timeline-container{padding-left:var(--tl-pad);--tl-line:var(--radius-sm);--tl-pad:var(--space-xl);position:relative}.stalux-archive-timeline-container:before{height:100%;width:var(--tl-line);border-radius:var(--border-medium);content:"";background:linear-gradient(to bottom, var(--accent-80p), var(--accent-40p), var(--white-20p));position:absolute;top:0;left:0}.stalux-archive-year-title{--dot:16px;color:var(--accent-90p);margin:0;font-size:1.8rem;position:relative}.stalux-archive-year-title:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:2;content:"";background-color:var(--accent-80p);border:3px solid var(--white-30p);border-radius:9999px;position:absolute;top:50%;transform:translateY(-50%)}.stalux-archive-post-item{--dot:6px;align-items:center;gap:.75rem;margin-bottom:1rem;padding:.35rem 0;display:flex;position:relative}.stalux-archive-post-item:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:1;content:"";background-color:var(--white-60p);border-radius:9999px;position:absolute;top:50%;transform:translateY(-50%)}.stalux-archive-month-title{--dot:10px;margin:0 0 .25rem;font-size:1rem;position:relative}.stalux-archive-month-title:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:1;content:"";background-color:#fff9;border-radius:9999px;position:absolute;top:50%;transform:translateY(-50%)}@supports not (content-visibility:auto){.stalux-archive-month-section,.stalux-archive-post-list{contain-intrinsic-block-size:auto}}@media (width<=768px){.stalux-archive-container{padding:1.5rem .5rem 2rem 1.5rem}.stalux-archive-page-header,.stalux-archive-year-section{margin-bottom:2rem}.stalux-archive-timeline-container{--tl-pad:1.5rem}.stalux-archive-year-title{--dot:14px;font-size:1.6rem}.stalux-archive-month-title{font-size:1.2rem}}@media (width<=480px){.stalux-archive-container{padding:1.5rem .5rem 2rem 1.2rem}.stalux-archive-timeline-container{--tl-pad:1.2rem;--tl-line:3px}.stalux-archive-year-title{--dot:12px;font-size:1.4rem}.stalux-archive-month-title{font-size:1.1rem}}}@layer utilities,theme,base;
2
+ @layer components{.stalux-archive-month-section{content-visibility:auto;contain-intrinsic-block-size:auto var(--archive-estimate,600px);margin-left:calc(-1 * var(--tl-pad));padding-left:var(--tl-pad);margin-bottom:1.75rem;position:relative}.stalux-archive-post-list{content-visibility:auto;contain-intrinsic-block-size:auto var(--archive-group-estimate,576px);margin:0 0 0 calc(-1 * var(--tl-pad));padding:0 0 0 var(--tl-pad);list-style-type:none}.stalux-archive-year-section{margin-bottom:var(--space-2xl);position:relative}.stalux-archive-container{max-width:1000px;padding:var(--space-xl) var(--space-md);margin:0 auto}.stalux-archives-archives-container{max-width:1000px;margin:0 auto;padding:2rem 1rem}.stalux-archive-year-count{background-color:var(--accent-20p);color:var(--white-90p);border-radius:1rem;margin-left:1rem;padding:.3rem .8rem;font-size:.9rem}.stalux-archive-year-header{flex-wrap:wrap;align-items:center;row-gap:.5rem;margin-bottom:1.25rem;display:flex}.stalux-archive-no-posts{background-color:var(--black-10p);text-align:center;width:100%;color:var(--white-70p);border-radius:.5rem;padding:3rem 1rem;font-size:1.2rem}.stalux-archive-post-date{min-width:90px;color:var(--white-60p);font-size:.85rem}.stalux-archive-post-link{text-decoration:none}.stalux-archive-description{box-sizing:border-box;z-index:10000;overflow-wrap:anywhere;width:min(320px,100vw - 32px);max-height:min(320px,100vh - 32px);color:var(--white-90p);border:1px solid var(--accent-40p);box-shadow:var(--shadow-sidebar);background:#18181bf5;border-radius:.75rem;margin:0;padding:1rem;font-size:.9rem;line-height:1.65;position:fixed;inset:auto;overflow-y:auto}.stalux-archive-description[hidden]{display:none}.stalux-archive-timeline-container{padding-left:var(--tl-pad);--tl-line:3px;--tl-pad:var(--space-xl);position:relative}.stalux-archive-timeline-container:before{height:100%;width:var(--tl-line);border-radius:var(--border-medium);content:"";background:linear-gradient(to bottom, var(--accent-80p), var(--accent-40p), var(--white-20p));position:absolute;top:0;left:0}.stalux-archive-year-title{--dot:16px;color:var(--accent-90p);margin:0;font-size:1.8rem;line-height:1.35;position:relative}.stalux-archive-year-title:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:2;content:"";background-color:var(--accent-80p);border:3px solid var(--white-30p);border-radius:9999px;position:absolute;top:.675em;transform:translateY(-50%)}.stalux-archive-post-item{--dot:6px;grid-template-columns:100px minmax(0,1fr);align-items:flex-start;gap:.75rem;margin-bottom:.6rem;padding:.4rem 0;line-height:1.65;display:grid;position:relative}.stalux-archive-post-item:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:1;content:"";background-color:var(--white-60p);border-radius:9999px;position:absolute;top:1.225rem;transform:translateY(-50%)}.stalux-archive-month-title{--dot:10px;color:var(--white-90p);margin:0 0 .6rem;font-size:1.05rem;font-weight:600;line-height:1.5;position:relative}.stalux-archive-month-title:before{box-sizing:border-box;width:var(--dot);height:var(--dot);left:calc(var(--tl-line) / 2 - var(--tl-pad) - var(--dot) / 2);z-index:1;content:"";background-color:#fff9;border-radius:9999px;position:absolute;top:.75em;transform:translateY(-50%)}.stalux-archive-post-link{overflow-wrap:anywhere;border-radius:3px;justify-self:start;min-width:0;max-width:100%;transition:color .16s}.stalux-archive-post-link:focus-visible{outline:2px solid var(--accent-90p);outline-offset:4px}.stalux-archive-post-date{font-variant-numeric:tabular-nums;white-space:nowrap;line-height:1.65rem}@media (hover:hover) and (pointer:fine){.stalux-archive-post-link:hover{color:var(--accent-90p);text-underline-offset:4px;text-decoration:none}}@supports not (content-visibility:auto){.stalux-archive-month-section,.stalux-archive-post-list{contain-intrinsic-block-size:auto}}@media (width<=768px){.stalux-archive-container{padding:1.5rem .5rem 2rem 1.5rem}.stalux-archive-page-header,.stalux-archive-year-section{margin-bottom:2rem}.stalux-archive-timeline-container{--tl-pad:1.5rem}.stalux-archive-year-title{--dot:14px;font-size:1.6rem}.stalux-archive-month-title{font-size:1.2rem}}@media (width<=600px){.stalux-archive-post-item{grid-template-columns:minmax(0,1fr);gap:.1rem}}@media (width<=480px){.stalux-archive-container{padding:1.5rem .5rem 2rem 1.2rem}.stalux-archive-timeline-container{--tl-pad:1.2rem;--tl-line:3px}.stalux-archive-year-title{--dot:12px;font-size:1.4rem}.stalux-archive-month-title{font-size:1.1rem}}@media (prefers-reduced-motion:reduce){.stalux-archive-post-link{transition:none}}@media print{.stalux-archive-description{display:none}.stalux-archive-month-section,.stalux-archive-post-list{content-visibility:visible;contain-intrinsic-block-size:none}}}@layer utilities,theme,base;
3
3
  /*$vite$:1*/