@xingwangzhe/stalux 1.48.0 → 1.49.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 +3 -1
- package/package.json +1 -1
- package/src/components/stalux/links/linkCard.astro +5 -1
- package/src/components/stalux/links/linkList.astro +2 -0
- package/src/components/stalux/posts/postContent.astro +1 -0
- package/src/index.ts +3 -0
- package/src/plugins/image-dimensions.ts +34 -0
- package/src/scripts/analytics-runtime.ts +103 -4
- package/src/scripts/link-icons.ts +54 -0
- package/src/scripts/photoswipe.ts +4 -3
- package/src/scripts/random-posts.ts +27 -1
- package/src/styles/generated-links.css +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ All HTML routes include a circular back-to-top button in the footer. It appears
|
|
|
14
14
|
|
|
15
15
|
The homepage typewriter is built into Stalux and continues to use the `items` string list in `stalux/config/typetexts.yml`, with no new configuration. Defaults are 100ms per typed grapheme, 33ms per deleted grapheme, a 1500ms hold, and a 500ms pause after deletion. It pauses outside the viewport or in background tabs and cleans up on soft navigation. Reduced motion displays whole messages without a blinking cursor. Without JavaScript, the first message remains visible; screen readers and print receive the complete list.
|
|
16
16
|
|
|
17
|
-
Standalone images written as `` receive a `figure` and `figcaption` through a build-time satteri HAST plugin. Captions appear as small, centered gray text below the image, preserving image alt text and the PhotoSwipe lightbox. No configuration or browser script is required. Empty descriptions, inline images, multiple images in one paragraph, and existing figures are left unchanged. Captions are plain text with automatic escaping.
|
|
17
|
+
Standalone images written as `` receive a `figure` and `figcaption` through a build-time satteri HAST plugin. Captions appear as small, centered gray text below the image, preserving image alt text and the PhotoSwipe lightbox. No configuration or browser script is required. Empty descriptions, inline images, multiple images in one paragraph, and existing figures are left unchanged. Captions are plain text with automatic escaping. External images can declare intrinsic size in the markdown title: ``. The build turns it into `width`/`height`, removes the tooltip title, and gives PhotoSwipe matching `data-pswp-width/height`; images without a size declaration keep their previous output.
|
|
18
|
+
|
|
19
|
+
Friend-link icons are requested only when they approach the viewport (400px prefetch margin), instead of the browser's large native lazy window pulling every third-party avatar on the page. A failed icon keeps a placeholder disc and never affects the text content. The Google Analytics SDK loads after the first screen settles (first input, 600ms of stable LCP, or six seconds after parsing), while `page_view` snapshots and the dataLayer queue are still written during navigation without waiting for the SDK. The random-posts sidebar likewise requests its index only near the viewport.
|
|
18
20
|
|
|
19
21
|
Code blocks default to the single `dark-plus` theme, with system color-scheme switching and theme selectors disabled. To opt into another theme or switching behavior, explicitly override `themes`, `useDarkModeMediaQuery`, and `themeCssSelector` through `expressiveCode`. This only changes generated code styles and adds no telemetry.
|
|
20
22
|
|
package/package.json
CHANGED
|
@@ -39,9 +39,13 @@ const icon = link.icon ?? link.avatar ?? "/favicon.ico";
|
|
|
39
39
|
<div class="stalux-link-card">
|
|
40
40
|
<div class="stalux-link-card-avatar">
|
|
41
41
|
<img
|
|
42
|
-
src=
|
|
42
|
+
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
43
|
+
data-src={icon}
|
|
44
|
+
data-stalux-link-icon
|
|
43
45
|
alt={t("links.iconAlt", { name: title })}
|
|
44
46
|
loading="lazy"
|
|
47
|
+
decoding="async"
|
|
48
|
+
fetchpriority="low"
|
|
45
49
|
width="80"
|
|
46
50
|
height="80"
|
|
47
51
|
>
|
|
@@ -128,6 +128,7 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
|
|
|
128
128
|
<!-- Article Body -->
|
|
129
129
|
<section
|
|
130
130
|
class="stalux-post-body stalux-prose"
|
|
131
|
+
data-stalux-gallery-body
|
|
131
132
|
data-stalux-motion-content
|
|
132
133
|
data-pagefind-body
|
|
133
134
|
data-stalux-post-id={props.postId === undefined ? undefined : String(props.postId)}
|
package/src/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ import { createViteAliases } from "./internal/vite-aliases";
|
|
|
64
64
|
import { webMcpMutationsPlugin } from "./internal/webmcp-mutations";
|
|
65
65
|
import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
|
|
66
66
|
import { imageCaptions } from "./plugins/image-captions";
|
|
67
|
+
import { imageDimensions } from "./plugins/image-dimensions";
|
|
67
68
|
import { temml } from "./plugins/satteri-temml";
|
|
68
69
|
import { describeError } from "./utils/diagnostics";
|
|
69
70
|
|
|
@@ -293,6 +294,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
293
294
|
}),
|
|
294
295
|
seen,
|
|
295
296
|
);
|
|
297
|
+
// Dimensions must land before PhotoSwipe copies them onto the link.
|
|
298
|
+
appendUniquePlugin(processorOptions.hastPlugins, imageDimensions, seen);
|
|
296
299
|
appendUniquePlugin(processorOptions.hastPlugins, photoswipe(), seen);
|
|
297
300
|
appendUniquePlugin(processorOptions.hastPlugins, imageCaptions, seen);
|
|
298
301
|
appendUniquePlugin(processorOptions.hastPlugins, featureFlagsHast, seen);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineHastPlugin } from "satteri";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Markdown 图片标题里的 `宽x高` 是外部图片的尺寸声明,不是给读者看的提示。
|
|
5
|
+
*
|
|
6
|
+
* 外部图片无法在构建期读取像素尺寸,浏览器只能等下载完才知道比例,正文会被
|
|
7
|
+
* 推开(CLS)。写成 `` 后,这里在 HAST 阶段补上
|
|
8
|
+
* width/height,PhotoSwipe 也能拿到 data-pswp-width/height;标题本身随即移除。
|
|
9
|
+
*/
|
|
10
|
+
const DIMENSION_TITLE = /^(\d{1,6})\s*[x×]\s*(\d{1,6})$/u;
|
|
11
|
+
|
|
12
|
+
export const imageDimensions = defineHastPlugin({
|
|
13
|
+
name: "stalux-image-dimensions",
|
|
14
|
+
element: {
|
|
15
|
+
filter: ["img"],
|
|
16
|
+
visit(node, ctx) {
|
|
17
|
+
const title = node.properties?.title;
|
|
18
|
+
if (typeof title !== "string") return;
|
|
19
|
+
const match = DIMENSION_TITLE.exec(title.trim());
|
|
20
|
+
if (!match) return;
|
|
21
|
+
const width = Number(match[1]);
|
|
22
|
+
const height = Number(match[2]);
|
|
23
|
+
if (!(width > 0 && height > 0)) return;
|
|
24
|
+
if (node.properties?.width === undefined) {
|
|
25
|
+
ctx.setProperty(node, "width", String(width));
|
|
26
|
+
}
|
|
27
|
+
if (node.properties?.height === undefined) {
|
|
28
|
+
ctx.setProperty(node, "height", String(height));
|
|
29
|
+
}
|
|
30
|
+
// 尺寸声明已转成属性,不再让浏览器把它当成悬浮提示。
|
|
31
|
+
ctx.setProperty(node, "title", undefined);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -202,6 +202,105 @@ function initializeClarity(id: string): void {
|
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* GA's SDK is 195 KB compressed / ~610 KB parsed, larger than every first-party
|
|
207
|
+
* asset combined. The dataLayer queue and the page_view snapshot do not need it:
|
|
208
|
+
* events are pushed immediately and consumed when the SDK finally arrives.
|
|
209
|
+
* Loading is gated on the same signals that mean "first paint is done" instead
|
|
210
|
+
* of `window.load`, which slow third-party resources can postpone forever.
|
|
211
|
+
*/
|
|
212
|
+
const GOOGLE_SDK_FLOOR_MS = 1200;
|
|
213
|
+
const GOOGLE_SDK_LCP_QUIET_MS = 600;
|
|
214
|
+
const GOOGLE_SDK_CEILING_MS = 6000;
|
|
215
|
+
let googleSdkStarted = false;
|
|
216
|
+
let googleGateInstalled = false;
|
|
217
|
+
let googleGateOpen = false;
|
|
218
|
+
|
|
219
|
+
function runBackgroundTask(callback: () => void): void {
|
|
220
|
+
const scheduler = (
|
|
221
|
+
window as Window & {
|
|
222
|
+
scheduler?: {
|
|
223
|
+
postTask: (
|
|
224
|
+
task: () => void,
|
|
225
|
+
options: { priority: "background" },
|
|
226
|
+
) => Promise<unknown>;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
).scheduler;
|
|
230
|
+
if (scheduler?.postTask) {
|
|
231
|
+
void scheduler
|
|
232
|
+
.postTask(callback, { priority: "background" })
|
|
233
|
+
.catch(() => setTimeout(callback, 0));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setTimeout(callback, 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function startGoogleSdk(): void {
|
|
240
|
+
if (googleSdkStarted || !state.googleId) return;
|
|
241
|
+
googleSdkStarted = true;
|
|
242
|
+
void loadTrackerScript(
|
|
243
|
+
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(state.googleId)}`,
|
|
244
|
+
{ "data-stalux-google": "true" },
|
|
245
|
+
).catch(() => logger.debug("Google SDK unavailable; queued events retained"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Three independent doors: first input, LCP quiet, and a hard ceiling after parsing. */
|
|
249
|
+
function installGoogleGate(): void {
|
|
250
|
+
if (googleGateInstalled) return;
|
|
251
|
+
googleGateInstalled = true;
|
|
252
|
+
const startedAt = performance.now();
|
|
253
|
+
const floorAt = startedAt + GOOGLE_SDK_FLOOR_MS;
|
|
254
|
+
const ceilingAt = startedAt + GOOGLE_SDK_CEILING_MS;
|
|
255
|
+
let quietTimer: ReturnType<typeof setTimeout> | undefined;
|
|
256
|
+
let ceilingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
257
|
+
let observer: PerformanceObserver | undefined;
|
|
258
|
+
|
|
259
|
+
const cleanup = () => {
|
|
260
|
+
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"]) {
|
|
261
|
+
window.removeEventListener(type, handleInteraction);
|
|
262
|
+
}
|
|
263
|
+
if (quietTimer !== undefined) clearTimeout(quietTimer);
|
|
264
|
+
if (ceilingTimer !== undefined) clearTimeout(ceilingTimer);
|
|
265
|
+
observer?.disconnect();
|
|
266
|
+
observer = undefined;
|
|
267
|
+
};
|
|
268
|
+
const open = () => {
|
|
269
|
+
if (googleGateOpen) return;
|
|
270
|
+
googleGateOpen = true;
|
|
271
|
+
cleanup();
|
|
272
|
+
// Never compete with the input that just opened the gate.
|
|
273
|
+
runBackgroundTask(startGoogleSdk);
|
|
274
|
+
};
|
|
275
|
+
function handleInteraction(): void {
|
|
276
|
+
open();
|
|
277
|
+
}
|
|
278
|
+
const scheduleQuiet = () => {
|
|
279
|
+
if (googleGateOpen) return;
|
|
280
|
+
if (quietTimer !== undefined) clearTimeout(quietTimer);
|
|
281
|
+
const wait = Math.max(GOOGLE_SDK_LCP_QUIET_MS, floorAt - performance.now());
|
|
282
|
+
quietTimer = setTimeout(() => {
|
|
283
|
+
quietTimer = undefined;
|
|
284
|
+
if (performance.now() >= floorAt) open();
|
|
285
|
+
else scheduleQuiet();
|
|
286
|
+
}, wait);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if ("PerformanceObserver" in window) {
|
|
290
|
+
try {
|
|
291
|
+
observer = new window.PerformanceObserver(scheduleQuiet);
|
|
292
|
+
observer.observe({ type: "largest-contentful-paint", buffered: true });
|
|
293
|
+
} catch {
|
|
294
|
+
// Older engines may know the constructor but not this entry type.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"]) {
|
|
298
|
+
window.addEventListener(type, handleInteraction, { passive: true, once: true });
|
|
299
|
+
}
|
|
300
|
+
ceilingTimer = setTimeout(open, Math.max(0, ceilingAt - startedAt));
|
|
301
|
+
if (document.hidden) open();
|
|
302
|
+
}
|
|
303
|
+
|
|
205
304
|
function initializeTrackers(): void {
|
|
206
305
|
const config = document.body?.dataset;
|
|
207
306
|
if (!config) return;
|
|
@@ -218,10 +317,10 @@ function initializeTrackers(): void {
|
|
|
218
317
|
host.gtag("js", new Date());
|
|
219
318
|
host.gtag("config", state.googleId, { send_page_view: false });
|
|
220
319
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
320
|
+
installGoogleGate();
|
|
321
|
+
// A navigation can expose the measurement ID after the first document
|
|
322
|
+
// already opened its gate; in that case load immediately.
|
|
323
|
+
if (googleGateOpen) startGoogleSdk();
|
|
225
324
|
}
|
|
226
325
|
if (config.staluxClarityId) initializeClarity(config.staluxClarityId);
|
|
227
326
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link-page icons come from arbitrary third-party hosts. Native lazy loading
|
|
3
|
+
* still fetches everything within the browser's large lazy threshold (up to
|
|
4
|
+
* 3000px), and a single link list therefore pulled several megabytes of
|
|
5
|
+
* avatars that the reader never reached. Load only icons near the viewport.
|
|
6
|
+
*/
|
|
7
|
+
import { createClientLogger } from "./logger";
|
|
8
|
+
import { registerPageLifecycle } from "./page-runtime";
|
|
9
|
+
|
|
10
|
+
const logger = createClientLogger("link-icons");
|
|
11
|
+
|
|
12
|
+
function reveal(image: HTMLImageElement): void {
|
|
13
|
+
const source = image.dataset.src;
|
|
14
|
+
if (!source) return;
|
|
15
|
+
delete image.dataset.src;
|
|
16
|
+
image.addEventListener(
|
|
17
|
+
"error",
|
|
18
|
+
() => {
|
|
19
|
+
// Keep the reserved box; the stylesheet draws a neutral disc instead.
|
|
20
|
+
image.removeAttribute("src");
|
|
21
|
+
image.dataset.staluxLinkIconFailed = "true";
|
|
22
|
+
logger.debug("link icon failed to load; placeholder retained");
|
|
23
|
+
},
|
|
24
|
+
{ once: true },
|
|
25
|
+
);
|
|
26
|
+
image.src = source;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mountLinkIcons(): (() => void) | undefined {
|
|
30
|
+
const images = Array.from(
|
|
31
|
+
document.querySelectorAll<HTMLImageElement>("img[data-stalux-link-icon]"),
|
|
32
|
+
);
|
|
33
|
+
if (!images.length) return;
|
|
34
|
+
|
|
35
|
+
if (!("IntersectionObserver" in window)) {
|
|
36
|
+
for (const image of images) reveal(image);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const observer = new IntersectionObserver(
|
|
41
|
+
(entries) => {
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!entry.isIntersecting) continue;
|
|
44
|
+
observer.unobserve(entry.target);
|
|
45
|
+
reveal(entry.target as HTMLImageElement);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ rootMargin: "400px 0px" },
|
|
49
|
+
);
|
|
50
|
+
for (const image of images) observer.observe(image);
|
|
51
|
+
return () => observer.disconnect();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerPageLifecycle("link-icons", mountLinkIcons);
|
|
@@ -6,6 +6,9 @@ import { createRetryableInitializer } from "./retryable-initializer";
|
|
|
6
6
|
const logger = createClientLogger("photoswipe");
|
|
7
7
|
|
|
8
8
|
const GALLERY_SELECTOR = "a.pswp-gallery-item";
|
|
9
|
+
// The article header, summary and keyword span also carry data-pagefind-body.
|
|
10
|
+
// Selecting the first of those gave PhotoSwipe an empty gallery (counter "1 / 0").
|
|
11
|
+
const GALLERY_CONTAINER_SELECTOR = "[data-stalux-gallery='true'] [data-stalux-gallery-body]";
|
|
9
12
|
|
|
10
13
|
async function enrichPhotoSwipeData(container: Element, signal: AbortSignal): Promise<void> {
|
|
11
14
|
const links = container.querySelectorAll<HTMLAnchorElement>(GALLERY_SELECTOR);
|
|
@@ -40,9 +43,7 @@ async function enrichPhotoSwipeData(container: Element, signal: AbortSignal): Pr
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
registerPageLifecycle("photoswipe", () => {
|
|
43
|
-
const container = document.querySelector<HTMLElement>(
|
|
44
|
-
"[data-stalux-gallery='true'] [data-pagefind-body]",
|
|
45
|
-
);
|
|
46
|
+
const container = document.querySelector<HTMLElement>(GALLERY_CONTAINER_SELECTOR);
|
|
46
47
|
if (!container) return;
|
|
47
48
|
|
|
48
49
|
const controller = new AbortController();
|
|
@@ -106,21 +106,47 @@ export function mountRandomPosts(): (() => void) | undefined {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
|
+
let started = false;
|
|
110
|
+
let observer: IntersectionObserver | undefined;
|
|
111
|
+
const start = () => {
|
|
112
|
+
if (started || disposed) return;
|
|
113
|
+
started = true;
|
|
114
|
+
observer?.disconnect();
|
|
115
|
+
observer = undefined;
|
|
116
|
+
void refresh();
|
|
117
|
+
};
|
|
109
118
|
container.querySelector("#refresh-random-posts")?.addEventListener(
|
|
110
119
|
"click",
|
|
111
120
|
(event) => {
|
|
112
121
|
event.preventDefault();
|
|
122
|
+
if (!started) {
|
|
123
|
+
start();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
113
126
|
void refresh();
|
|
114
127
|
},
|
|
115
128
|
{ signal: controller.signal },
|
|
116
129
|
);
|
|
117
130
|
reducedMotion.addEventListener("change", cancelAnimation, { signal: controller.signal });
|
|
118
|
-
|
|
131
|
+
if ("IntersectionObserver" in window) {
|
|
132
|
+
// The sidebar sits below the article on phones; do not fetch its index
|
|
133
|
+
// until the reader is close enough to see it.
|
|
134
|
+
observer = new IntersectionObserver(
|
|
135
|
+
(entries) => {
|
|
136
|
+
if (entries.some((entry) => entry.isIntersecting)) start();
|
|
137
|
+
},
|
|
138
|
+
{ rootMargin: "300px 0px" },
|
|
139
|
+
);
|
|
140
|
+
observer.observe(container);
|
|
141
|
+
} else {
|
|
142
|
+
start();
|
|
143
|
+
}
|
|
119
144
|
return () => {
|
|
120
145
|
disposed = true;
|
|
121
146
|
revision++;
|
|
122
147
|
request?.abort();
|
|
123
148
|
controller.abort();
|
|
149
|
+
observer?.disconnect();
|
|
124
150
|
cancelAnimation();
|
|
125
151
|
container.removeAttribute("aria-busy");
|
|
126
152
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
@layer theme, base, components, utilities;
|
|
2
|
-
@layer components{.stalux-link-card{color:#fff;box-sizing:border-box;background:var(--surface-glass);width:100%;min-width:0;min-height:200px;box-shadow:var(--card-box-shadow);border-radius:10px;flex-direction:column;padding:16px;text-decoration:none;transition:transform .22s,background-color .22s,border-color .22s;display:flex;overflow:visible}.stalux-link-card-content{text-align:center;flex-direction:column;flex:1;display:flex}.stalux-links{box-sizing:border-box;grid-template-columns:repeat(auto-fill,minmax(min(180px,100%),1fr));gap:20px;width:100%;max-width:1600px;margin:30px auto;padding:0 1rem;list-style-type:none;display:grid}.stalux-link-card-avatar{justify-content:center;margin-bottom:12px;display:flex}.stalux-link-list-item{width:100%;min-width:0;display:flex}.stalux-link-shell{width:100%;min-width:0}.stalux-link-card-content{transition:transform .2s}.stalux-link-card-avatar img{object-fit:cover;border:2px solid var(--accent-60p);transform-origin:50%;border-radius:9999px;width:80px;height:80px;transition:transform .22s,border-color .22s}.stalux-link-card-title{white-space:nowrap;background:linear-gradient(90deg, var(--white-95p), var(--accent-90p));color:#0000;text-overflow:ellipsis;background-clip:text;margin:0 0 8px;font-size:1.1rem;overflow:hidden}.stalux-link-card-description{color:var(--white-78p);-webkit-line-clamp:3;text-overflow:ellipsis;-webkit-box-orient:vertical;margin:0;font-size:.95rem;line-height:1.45;display:-webkit-box;overflow:hidden}@media (1100px<=width<=1399px){.stalux-links{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (480px<=width<=767px){.stalux-links{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}}@media (768px<=width<=1099px){.stalux-links{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (hover:hover) and (pointer:fine){.stalux-link-shell:hover .stalux-link-card-content{transform:translateY(-2px)}.stalux-link-shell:hover .stalux-link-card{background:var(--white-05p);box-shadow:var(--card-box-shadow-hover)}.stalux-link-card-avatar:hover img{border-color:var(--accent-85p);transform:rotate(3deg)}}@media (width>=1400px){.stalux-links{grid-template-columns:repeat(5,minmax(0,1fr))}}@media (width<=480px){.stalux-link-card{padding:14px}.stalux-link-card-avatar img{width:70px;height:70px}.stalux-link-card-title{font-size:1rem}.stalux-link-card-description{font-size:.9rem}}@media (width<=479px){.stalux-links{grid-template-columns:1fr;gap:14px;margin:20px auto}}@media (prefers-reduced-motion:reduce) and (hover:hover) and (pointer:fine){.stalux-link-card-avatar:hover img{transition:none;animation:none;transform:none}}}@layer utilities,theme,base;
|
|
2
|
+
@layer components{.stalux-link-card{color:#fff;box-sizing:border-box;background:var(--surface-glass);width:100%;min-width:0;min-height:200px;box-shadow:var(--card-box-shadow);border-radius:10px;flex-direction:column;padding:16px;text-decoration:none;transition:transform .22s,background-color .22s,border-color .22s;display:flex;overflow:visible}.stalux-link-card-content{text-align:center;flex-direction:column;flex:1;display:flex}.stalux-links{box-sizing:border-box;grid-template-columns:repeat(auto-fill,minmax(min(180px,100%),1fr));gap:20px;width:100%;max-width:1600px;margin:30px auto;padding:0 1rem;list-style-type:none;display:grid}.stalux-link-card-avatar{justify-content:center;margin-bottom:12px;display:flex}.stalux-link-list-item{width:100%;min-width:0;display:flex}.stalux-link-shell{width:100%;min-width:0}.stalux-link-card-content{transition:transform .2s}.stalux-link-card-avatar img{object-fit:cover;border:2px solid var(--accent-60p);transform-origin:50%;border-radius:9999px;width:80px;height:80px;transition:transform .22s,border-color .22s}.stalux-link-card-avatar img[data-src],.stalux-link-card-avatar img[data-stalux-link-icon-failed]{background:var(--white-05p)}.stalux-link-card-title{white-space:nowrap;background:linear-gradient(90deg, var(--white-95p), var(--accent-90p));color:#0000;text-overflow:ellipsis;background-clip:text;margin:0 0 8px;font-size:1.1rem;overflow:hidden}.stalux-link-card-description{color:var(--white-78p);-webkit-line-clamp:3;text-overflow:ellipsis;-webkit-box-orient:vertical;margin:0;font-size:.95rem;line-height:1.45;display:-webkit-box;overflow:hidden}@media (1100px<=width<=1399px){.stalux-links{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (480px<=width<=767px){.stalux-links{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}}@media (768px<=width<=1099px){.stalux-links{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (hover:hover) and (pointer:fine){.stalux-link-shell:hover .stalux-link-card-content{transform:translateY(-2px)}.stalux-link-shell:hover .stalux-link-card{background:var(--white-05p);box-shadow:var(--card-box-shadow-hover)}.stalux-link-card-avatar:hover img{border-color:var(--accent-85p);transform:rotate(3deg)}}@media (width>=1400px){.stalux-links{grid-template-columns:repeat(5,minmax(0,1fr))}}@media (width<=480px){.stalux-link-card{padding:14px}.stalux-link-card-avatar img{width:70px;height:70px}.stalux-link-card-title{font-size:1rem}.stalux-link-card-description{font-size:.9rem}}@media (width<=479px){.stalux-links{grid-template-columns:1fr;gap:14px;margin:20px auto}}@media (prefers-reduced-motion:reduce) and (hover:hover) and (pointer:fine){.stalux-link-card-avatar:hover img{transition:none;animation:none;transform:none}}}@layer utilities,theme,base;
|
|
3
3
|
/*$vite$:1*/
|