@xingwangzhe/stalux 1.37.1 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/components/stalux/archives/toArchive.astro +5 -1
  3. package/src/components/stalux/common/Typewriter.astro +7 -1
  4. package/src/components/stalux/common/author.astro +2 -0
  5. package/src/components/stalux/common/date.astro +8 -1
  6. package/src/components/stalux/layout/PageHeading.astro +27 -0
  7. package/src/components/stalux/layout/footer.astro +6 -1
  8. package/src/components/stalux/layout/listPage.astro +2 -6
  9. package/src/components/stalux/links/socialMedia.astro +3 -0
  10. package/src/components/stalux/posts/ArticleSummary.astro +8 -2
  11. package/src/components/stalux/posts/PostTaxonomy.astro +10 -3
  12. package/src/components/stalux/posts/postCC.astro +3 -2
  13. package/src/components/stalux/posts/postContent.astro +46 -27
  14. package/src/components/stalux/posts/postLeft.astro +17 -3
  15. package/src/components/stalux/posts/postRight.astro +20 -4
  16. package/src/components/stalux/posts/postToc.astro +5 -2
  17. package/src/components/stalux/tags/tagHeader.astro +5 -1
  18. package/src/index.ts +47 -4
  19. package/src/internal/font-slices.ts +159 -24
  20. package/src/internal/page-output-cache.ts +3 -1
  21. package/src/internal/style-names.generated.ts +121 -120
  22. package/src/layouts/PostLayout.astro +12 -2
  23. package/src/layouts/Stalux.astro +14 -2
  24. package/src/pages/404.astro +0 -3
  25. package/src/pages/about.astro +3 -3
  26. package/src/pages/archives.astro +5 -7
  27. package/src/pages/categories/[category].astro +0 -3
  28. package/src/pages/categories/index.astro +2 -5
  29. package/src/pages/index.astro +6 -6
  30. package/src/pages/posts/[post].astro +0 -3
  31. package/src/pages/tags/[tag].astro +0 -3
  32. package/src/pages/tags/index.astro +2 -5
  33. package/src/pages/words.astro +5 -7
  34. package/src/scripts/navigation.ts +16 -1
  35. package/src/scripts/view-transitions.ts +46 -1
  36. package/src/styles/base.css +4 -3
  37. package/src/styles/generated.css +1 -1
  38. package/src/styles/motion.css +60 -0
@@ -2,8 +2,9 @@ import { styleNames } from "./style-names.generated.ts";
2
2
  /**
3
3
  * Build-time CJK font subsetting engine.
4
4
  *
5
- * LXGW WenKai is subset per rendered page with the native N-API package. Only
6
- * the independent Google Sans Code font uses Astro's local Fonts API.
5
+ * LXGW WenKai uses shared subsets for the persistent header and footer, then
6
+ * one content-addressed subset per rendered page for the remaining glyphs.
7
+ * Only the independent Google Sans Code font uses Astro's local Fonts API.
7
8
  *
8
9
  * Astro's local provider reads font files from disk (`readFile`), so no
9
10
  * network access is involved at build time — identical behavior on GitHub
@@ -12,7 +13,8 @@ import { styleNames } from "./style-names.generated.ts";
12
13
  *
13
14
  * Output layout:
14
15
  * node_modules/.astro/stalux-page-fonts/
15
- * page-{hash}.woff2 — page-specific content-addressed CJK subset
16
+ * page-{hash}.woff2 — page-specific content-addressed CJK subset
17
+ * shared-header/footer-*.woff2 — persistent shell glyph subsets
16
18
  */
17
19
 
18
20
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
@@ -87,7 +89,7 @@ function findFont(projectRoot: string, rel: string): string | undefined {
87
89
  // ---------------------------------------------------------------------------
88
90
 
89
91
  /**
90
- * Resolve the body-font source for native page subsetting and code fonts for
92
+ * Resolve the body-font source for native shell/page subsetting and code fonts for
91
93
  * Astro's local provider. Returns `null` when a required font is missing so
92
94
  * the caller can skip font injection (pages fall back to system fonts).
93
95
  *
@@ -130,8 +132,30 @@ export interface PageFontSubset {
130
132
  filename: string;
131
133
  sourcePath: string;
132
134
  cacheHit: boolean;
135
+ glyphCount: number;
133
136
  }
134
137
 
138
+ export type SharedFontGroup = "header" | "footer";
139
+
140
+ export interface SharedFontGlyphs {
141
+ header: Set<string>;
142
+ footer: Set<string>;
143
+ }
144
+
145
+ export interface SharedFontSubset {
146
+ group: SharedFontGroup;
147
+ family: string;
148
+ chars: string[];
149
+ filename: string;
150
+ sourcePath: string;
151
+ cacheHit: boolean;
152
+ }
153
+
154
+ const SHARED_FONT_FAMILIES: Record<SharedFontGroup, string> = {
155
+ header: "LXGW WenKai Header Subset",
156
+ footer: "LXGW WenKai Footer Subset",
157
+ };
158
+
135
159
  type HtmlNode = DefaultTreeAdapterMap["node"];
136
160
 
137
161
  const NON_RENDERED_ELEMENTS = new Set(["script", "style", "template", "title"]);
@@ -230,28 +254,96 @@ function isHiddenElement(attrs: Array<{ name: string; value: string }>): boolean
230
254
  );
231
255
  }
232
256
 
233
- /** Collect renderable body text nodes, including content inside collapsed controls. */
257
+ /** Collect renderable text nodes, including content inside collapsed controls. */
234
258
  const CONTROL_CHARACTER = /\p{C}/u;
235
259
  function addGlyphs(text: string, output: Set<string>): void {
236
260
  for (const char of text)
237
261
  if (!output.has(char) && !CONTROL_CHARACTER.test(char)) output.add(char);
238
262
  }
239
263
 
240
- function collectBodyText(node: HtmlNode, output: Set<string>): void {
264
+ function collectBodyText(node: HtmlNode, output: Set<string>, skipSharedComponents = false): void {
241
265
  if (node.nodeName === "#text" && "value" in node) {
242
266
  addGlyphs(node.value, output);
243
267
  return;
244
268
  }
245
269
 
246
270
  if (!("childNodes" in node)) return;
247
- if (
248
- "tagName" in node &&
249
- (NON_RENDERED_ELEMENTS.has(node.tagName) || isHiddenElement(node.attrs))
250
- ) {
251
- return;
271
+ if ("tagName" in node) {
272
+ const sharedGroup = node.attrs.find(
273
+ (attribute) => attribute.name === "data-stalux-font-group",
274
+ )?.value;
275
+ if (skipSharedComponents && (sharedGroup === "header" || sharedGroup === "footer")) {
276
+ return;
277
+ }
278
+ if (NON_RENDERED_ELEMENTS.has(node.tagName) || isHiddenElement(node.attrs)) return;
252
279
  }
253
280
 
254
- for (const child of node.childNodes) collectBodyText(child, output);
281
+ for (const child of node.childNodes) collectBodyText(child, output, skipSharedComponents);
282
+ }
283
+
284
+ /** Collect common shell glyphs separately so every page can reuse the same files. */
285
+ export function createSharedFontGlyphs(): SharedFontGlyphs {
286
+ const glyphs: SharedFontGlyphs = { header: new Set(), footer: new Set() };
287
+ addGlyphs(RESERVED_GLYPHS, glyphs.footer);
288
+ return glyphs;
289
+ }
290
+
291
+ export function collectSharedFontGlyphs(
292
+ document: DefaultTreeAdapterMap["document"],
293
+ output: SharedFontGlyphs,
294
+ ): void {
295
+ function visit(node: HtmlNode): void {
296
+ if ("tagName" in node) {
297
+ const group = node.attrs.find(
298
+ (attribute) => attribute.name === "data-stalux-font-group",
299
+ )?.value as SharedFontGroup | undefined;
300
+ if (group === "header" || group === "footer") {
301
+ collectBodyText(node, output[group]);
302
+ return;
303
+ }
304
+ }
305
+ if ("childNodes" in node) for (const child of node.childNodes) visit(child);
306
+ }
307
+ visit(document);
308
+ }
309
+
310
+ function sortGlyphs(glyphs: Iterable<string>): string[] {
311
+ return [...new Set(glyphs)].sort(
312
+ (a, b) => (a.codePointAt(0) as number) - (b.codePointAt(0) as number),
313
+ );
314
+ }
315
+
316
+ /** Build content-addressed header and footer subsets once for the whole site build. */
317
+ export async function writeSharedFontSubsetsAsync(
318
+ fontBuffer: Buffer,
319
+ glyphs: SharedFontGlyphs,
320
+ cacheDir: string,
321
+ ): Promise<SharedFontSubset[]> {
322
+ let prepared = preparedFonts.get(fontBuffer);
323
+ if (!prepared) {
324
+ prepared = new FontSubsetter(fontBuffer);
325
+ preparedFonts.set(fontBuffer, prepared);
326
+ }
327
+
328
+ const subsets: SharedFontSubset[] = [];
329
+ for (const group of ["header", "footer"] as const) {
330
+ const chars = sortGlyphs(glyphs[group]);
331
+ if (chars.length === 0) continue;
332
+ const result = await prepared.subsetAsync(chars.join(""), cacheDir);
333
+ subsets.push({
334
+ group,
335
+ family: SHARED_FONT_FAMILIES[group],
336
+ chars,
337
+ filename: `shared-${group}-${result.hash}.woff2`,
338
+ sourcePath: result.path,
339
+ cacheHit: result.cacheHit,
340
+ });
341
+ }
342
+ return subsets;
343
+ }
344
+
345
+ export function sharedFontCacheKey(subsets: readonly SharedFontSubset[]): string {
346
+ return subsets.map(({ group, chars }) => `${group}:${chars.join("")}`).join("\0");
255
347
  }
256
348
 
257
349
  function formatUnicodeRange(chars: string[]): string {
@@ -286,8 +378,9 @@ export function writePageFontSubset(
286
378
  publicUrlPrefix,
287
379
  input.chars,
288
380
  input.prepared.subset(input.chars.join(""), cacheDir),
381
+ [],
289
382
  );
290
- return { ...result, html: serialize(document) };
383
+ return { ...result, glyphCount: input.chars.length, html: serialize(document) };
291
384
  }
292
385
 
293
386
  export async function writePageFontSubsetAsync(
@@ -303,7 +396,8 @@ export async function writePageFontSubsetAsync(
303
396
  if (!input) return undefined;
304
397
  const result = await input.prepared.subsetAsync(input.chars.join(""), cacheDir);
305
398
  return {
306
- ...renderPageFont(document, publicUrlPrefix, input.chars, result),
399
+ ...renderPageFont(document, publicUrlPrefix, input.chars, result, []),
400
+ glyphCount: input.chars.length,
307
401
  html: serialize(document),
308
402
  };
309
403
  }
@@ -313,6 +407,7 @@ function preparePageFont(
313
407
  linkedStylesheetText: string,
314
408
  extraChars: string,
315
409
  document: DefaultTreeAdapterMap["document"],
410
+ excludedChars: ReadonlySet<string> = new Set(),
316
411
  ) {
317
412
  const body = findBody(document);
318
413
  if (!body) return undefined;
@@ -323,7 +418,10 @@ function preparePageFont(
323
418
  addGlyphs(linkedStylesheetText, glyphs);
324
419
  for (const style of collectInlineStyles(document))
325
420
  addGlyphs(cssGeneratedText(styleText(style)), glyphs);
326
- collectBodyText(body, glyphs);
421
+ collectBodyText(body, glyphs, true);
422
+ for (const char of excludedChars) glyphs.delete(char);
423
+ // Keep runtime digits available even when the shell subset does not contain them.
424
+ addGlyphs(RESERVED_GLYPHS, glyphs);
327
425
  const chars = [...glyphs].sort(
328
426
  (a, b) => (a.codePointAt(0) as number) - (b.codePointAt(0) as number),
329
427
  );
@@ -342,6 +440,7 @@ function renderPageFont(
342
440
  publicUrlPrefix: string,
343
441
  chars: string[],
344
442
  result: { hash: string; path: string; cacheHit: boolean },
443
+ sharedFonts: readonly SharedFontSubset[],
345
444
  ): Omit<PageFontSubset, "html"> {
346
445
  const filename = `page-${result.hash}.woff2`;
347
446
 
@@ -359,12 +458,20 @@ function renderPageFont(
359
458
  .map(styleText)
360
459
  .map((css) => css.match(/--font-code:\s*([^;}]+)/u)?.[1]?.trim())
361
460
  .find(Boolean);
362
- const codeStack = originalCodeStack
363
- ? `${originalCodeStack},"LXGW WenKai-Page Subset"`
364
- : `"LXGW WenKai-Page Subset",${fallbackStack}`;
461
+ const sharedStack = sharedFonts.map(({ family }) => `"${family}"`).join(",");
462
+ const pageFontStack = [sharedStack, '"LXGW WenKai-Page Subset"', fallbackStack]
463
+ .filter(Boolean)
464
+ .join(",");
465
+ const codeStack = originalCodeStack ? `${originalCodeStack},${pageFontStack}` : pageFontStack;
365
466
  // The source font is regular; advertise a weight range so browsers synthesize
366
467
  // bold weights from these same glyphs instead of falling back to system CJK.
367
- const css = `@font-face{font-family:"LXGW WenKai-Page Subset";src:url("${rel}") format("woff2");font-style:normal;font-weight:100 900;font-display:swap;unicode-range:${formatUnicodeRange(chars)}}:root{--font-body:"LXGW WenKai-Page Subset",${fallbackStack};--font-code:${codeStack}}body{font-family:var(--font-body),${fallbackStack}}`;
468
+ const sharedFaces = sharedFonts
469
+ .map(
470
+ ({ family, filename, chars: sharedChars }) =>
471
+ `@font-face{font-family:"${family}";src:url("${publicUrlPrefix}${filename}") format("woff2");font-style:normal;font-weight:100 900;font-display:swap;unicode-range:${formatUnicodeRange(sharedChars)}}`,
472
+ )
473
+ .join("");
474
+ const css = `${sharedFaces}@font-face{font-family:"LXGW WenKai-Page Subset";src:url("${rel}") format("woff2");font-style:normal;font-weight:100 900;font-display:swap;unicode-range:${formatUnicodeRange(chars)}}:root{--font-body:${pageFontStack};--font-code:${codeStack}}body{font-family:var(--font-body),${fallbackStack}}`;
368
475
  for (const style of styles) {
369
476
  for (const node of style.childNodes) {
370
477
  if (node.nodeName !== "#text" || !("value" in node)) continue;
@@ -398,11 +505,28 @@ function renderPageFont(
398
505
  { name: "href", value: rel },
399
506
  { name: "data-stalux-page-font-preload", value: "" },
400
507
  ]);
508
+ for (const sharedFont of sharedFonts) {
509
+ if (sharedFont.group !== "header") continue;
510
+ const sharedPreload = defaultTreeAdapter.createElement("link", htmlNamespaces.NS.HTML, [
511
+ { name: "rel", value: "preload" },
512
+ { name: "as", value: "font" },
513
+ { name: "type", value: "font/woff2" },
514
+ { name: "crossorigin", value: "anonymous" },
515
+ { name: "href", value: `${publicUrlPrefix}${sharedFont.filename}` },
516
+ { name: "data-stalux-shared-font-preload", value: sharedFont.group },
517
+ ]);
518
+ defaultTreeAdapter.appendChild(head, sharedPreload);
519
+ }
401
520
  defaultTreeAdapter.appendChild(head, preload);
402
521
  defaultTreeAdapter.insertText(style, css);
403
522
  defaultTreeAdapter.appendChild(head, style);
404
523
  }
405
- return { filename, sourcePath: result.path, cacheHit: result.cacheHit };
524
+ return {
525
+ filename,
526
+ sourcePath: result.path,
527
+ cacheHit: result.cacheHit,
528
+ glyphCount: chars.length,
529
+ };
406
530
  }
407
531
 
408
532
  /** Styles are selected structurally, so examples in scripts/comments/templates are untouched. */
@@ -430,7 +554,11 @@ export function removePageFontStyles(document: DefaultTreeAdapterMap["document"]
430
554
  if (
431
555
  "tagName" in node &&
432
556
  node.tagName === "link" &&
433
- node.attrs.some((attr) => attr.name === "data-stalux-page-font-preload")
557
+ node.attrs.some(
558
+ (attr) =>
559
+ attr.name === "data-stalux-page-font-preload" ||
560
+ attr.name === "data-stalux-shared-font-preload",
561
+ )
434
562
  ) {
435
563
  defaultTreeAdapter.detachNode(node);
436
564
  return;
@@ -451,15 +579,18 @@ export async function writePageFontSubsetIntoDocumentAsync(
451
579
  cacheDir: string,
452
580
  publicUrlPrefix = "/_astro/fonts/",
453
581
  linkedStylesheetText = "",
582
+ sharedFonts: readonly SharedFontSubset[] = [],
454
583
  ): Promise<Omit<PageFontSubset, "html"> | undefined> {
455
- const input = preparePageFont(fontBuffer, linkedStylesheetText, "", document);
584
+ const sharedChars = new Set(sharedFonts.flatMap(({ chars }) => chars));
585
+ const input = preparePageFont(fontBuffer, linkedStylesheetText, "", document, sharedChars);
456
586
  if (!input) return undefined;
457
587
  const result = await input.prepared.subsetAsync(input.chars.join(""), cacheDir);
458
- const rendered = renderPageFont(document, publicUrlPrefix, input.chars, result);
588
+ const rendered = renderPageFont(document, publicUrlPrefix, input.chars, result, sharedFonts);
459
589
  return {
460
590
  filename: rendered.filename,
461
591
  sourcePath: rendered.sourcePath,
462
592
  cacheHit: rendered.cacheHit,
593
+ glyphCount: rendered.glyphCount,
463
594
  };
464
595
  }
465
596
 
@@ -470,7 +601,11 @@ export function clearPageFontSubsets(
470
601
  ): void {
471
602
  if (!existsSync(outputDir)) return;
472
603
  for (const name of readdirSync(outputDir)) {
473
- if (name.startsWith("page-") && name.endsWith(".woff2") && !referencedFiles.has(name)) {
604
+ const isSubset =
605
+ name.startsWith("page-") ||
606
+ name.startsWith("shared-header-") ||
607
+ name.startsWith("shared-footer-");
608
+ if (isSubset && name.endsWith(".woff2") && !referencedFiles.has(name)) {
474
609
  unlinkSync(join(outputDir, name));
475
610
  }
476
611
  }
@@ -15,9 +15,11 @@ export async function createPageOutputCache(
15
15
  dependencies: readonly string[],
16
16
  font?: Buffer,
17
17
  assets: readonly string[] = [],
18
+ contentKey = "",
18
19
  ) {
19
- const hash = createHash("sha256").update("stalux-page-output-v1\0");
20
+ const hash = createHash("sha256").update("stalux-page-output-v2\0");
20
21
  if (font) hash.update(font);
22
+ if (contentKey) hash.update("\0content-key\0").update(contentKey);
21
23
  for (const dependency of [...dependencies].sort()) {
22
24
  hash.update(dependency).update("\0");
23
25
  hash.update(await fs.readFile(dependency)).update("\0");
@@ -89,84 +89,84 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
89
89
  "stalux-footer": "s2d",
90
90
  "stalux-footer-content": "s2e",
91
91
  "stalux-home-body": "s2f",
92
- "stalux-home-navs": "s2g",
93
- "stalux-home-title": "s2h",
94
- "stalux-license-author": "s2i",
95
- "stalux-license-box": "s2j",
96
- "stalux-license-content": "s2k",
97
- "stalux-license-icon": "s2l",
98
- "stalux-license-license": "s2m",
99
- "stalux-license-title": "s2n",
100
- "stalux-license-url": "s2o",
101
- "stalux-license-watermark": "s2p",
102
- "stalux-link-card": "s2q",
103
- "stalux-link-card-avatar": "s2r",
104
- "stalux-link-card-content": "s2s",
105
- "stalux-link-card-description": "s2t",
106
- "stalux-link-card-title": "s2u",
107
- "stalux-link-list-item": "s2v",
108
- "stalux-link-shell": "s2w",
109
- "stalux-links": "s2x",
110
- "stalux-main": "s2y",
111
- "stalux-nav-container": "s2z",
112
- "stalux-nav-item": "s30",
113
- "stalux-nav-link": "s31",
114
- "stalux-nav-list": "s32",
115
- "stalux-nav-overlay": "s33",
116
- "stalux-nav-scroll-btn": "s34",
117
- "stalux-nav-text": "s35",
118
- "stalux-nav-toggle": "s36",
119
- "stalux-page-description": "s37",
120
- "stalux-page-title": "s38",
121
- "stalux-pagination": "s39",
122
- "stalux-pagination-arrow": "s3a",
123
- "stalux-pagination-content": "s3b",
124
- "stalux-pagination-disabled": "s3c",
125
- "stalux-pagination-inner": "s3d",
126
- "stalux-pagination-label": "s3e",
127
- "stalux-pagination-link": "s3f",
128
- "stalux-pagination-next-post": "s3g",
129
- "stalux-pagination-prev-post": "s3h",
130
- "stalux-pagination-title": "s3i",
131
- "stalux-post-article-footer": "s3j",
132
- "stalux-post-article-header": "s3k",
133
- "stalux-post-body": "s3l",
134
- "stalux-post-card": "s3m",
135
- "stalux-post-card-shell": "s3n",
136
- "stalux-post-footer-content": "s3o",
137
- "stalux-post-layout": "s3p",
138
- "stalux-post-list-container": "s3q",
139
- "stalux-post-meta-item": "s3r",
140
- "stalux-post-meta-row": "s3s",
141
- "stalux-post-metadata": "s3t",
142
- "stalux-post-page": "s3u",
143
- "stalux-post-page-stats": "s3v",
144
- "stalux-post-reading-time": "s3w",
145
- "stalux-post-source-link": "s3x",
146
- "stalux-post-source-link-row": "s3y",
147
- "stalux-post-taxonomy-container": "s3z",
148
- "stalux-post-taxonomy-empty": "s40",
149
- "stalux-post-taxonomy-item-column": "s41",
150
- "stalux-post-taxonomy-item-pill": "s42",
151
- "stalux-post-taxonomy-list": "s43",
152
- "stalux-post-taxonomy-list-column": "s44",
153
- "stalux-post-taxonomy-list-wrap": "s45",
154
- "stalux-post-taxonomy-title": "s46",
155
- "stalux-post-title": "s47",
156
- "stalux-prose": "s48",
157
- "stalux-random-container": "s49",
158
- "stalux-random-error": "s4a",
159
- "stalux-random-header": "s4b",
160
- "stalux-random-list": "s4c",
161
- "stalux-random-loading": "s4d",
162
- "stalux-random-no-more": "s4e",
163
- "stalux-random-refresh": "s4f",
164
- "stalux-random-rotating": "s4g",
165
- "stalux-random-title": "s4h",
166
- "stalux-root": "s4i",
167
- "stalux-sidebar": "s4j",
168
- "stalux-sidebar-container": "s4k",
169
- "stalux-sidebar-right": "s4l",
92
+ "stalux-home-title": "s2g",
93
+ "stalux-license-author": "s2h",
94
+ "stalux-license-box": "s2i",
95
+ "stalux-license-content": "s2j",
96
+ "stalux-license-icon": "s2k",
97
+ "stalux-license-license": "s2l",
98
+ "stalux-license-title": "s2m",
99
+ "stalux-license-url": "s2n",
100
+ "stalux-license-watermark": "s2o",
101
+ "stalux-link-card": "s2p",
102
+ "stalux-link-card-avatar": "s2q",
103
+ "stalux-link-card-content": "s2r",
104
+ "stalux-link-card-description": "s2s",
105
+ "stalux-link-card-title": "s2t",
106
+ "stalux-link-list-item": "s2u",
107
+ "stalux-link-shell": "s2v",
108
+ "stalux-links": "s2w",
109
+ "stalux-main": "s2x",
110
+ "stalux-nav-container": "s2y",
111
+ "stalux-nav-item": "s2z",
112
+ "stalux-nav-link": "s30",
113
+ "stalux-nav-list": "s31",
114
+ "stalux-nav-overlay": "s32",
115
+ "stalux-nav-scroll-btn": "s33",
116
+ "stalux-nav-text": "s34",
117
+ "stalux-nav-toggle": "s35",
118
+ "stalux-page-description": "s36",
119
+ "stalux-page-title": "s37",
120
+ "stalux-pagination": "s38",
121
+ "stalux-pagination-arrow": "s39",
122
+ "stalux-pagination-content": "s3a",
123
+ "stalux-pagination-disabled": "s3b",
124
+ "stalux-pagination-inner": "s3c",
125
+ "stalux-pagination-label": "s3d",
126
+ "stalux-pagination-link": "s3e",
127
+ "stalux-pagination-next-post": "s3f",
128
+ "stalux-pagination-prev-post": "s3g",
129
+ "stalux-pagination-title": "s3h",
130
+ "stalux-post-article-footer": "s3i",
131
+ "stalux-post-article-header": "s3j",
132
+ "stalux-post-body": "s3k",
133
+ "stalux-post-card": "s3l",
134
+ "stalux-post-card-shell": "s3m",
135
+ "stalux-post-footer-content": "s3n",
136
+ "stalux-post-layout": "s3o",
137
+ "stalux-post-list-container": "s3p",
138
+ "stalux-post-meta-item": "s3q",
139
+ "stalux-post-meta-row": "s3r",
140
+ "stalux-post-metadata": "s3s",
141
+ "stalux-post-page": "s3t",
142
+ "stalux-post-page-stats": "s3u",
143
+ "stalux-post-reading-time": "s3v",
144
+ "stalux-post-source-link": "s3w",
145
+ "stalux-post-source-link-row": "s3x",
146
+ "stalux-post-taxonomy-container": "s3y",
147
+ "stalux-post-taxonomy-empty": "s3z",
148
+ "stalux-post-taxonomy-item-column": "s40",
149
+ "stalux-post-taxonomy-item-pill": "s41",
150
+ "stalux-post-taxonomy-list": "s42",
151
+ "stalux-post-taxonomy-list-column": "s43",
152
+ "stalux-post-taxonomy-list-wrap": "s44",
153
+ "stalux-post-taxonomy-title": "s45",
154
+ "stalux-post-title": "s46",
155
+ "stalux-prose": "s47",
156
+ "stalux-random-container": "s48",
157
+ "stalux-random-error": "s49",
158
+ "stalux-random-header": "s4a",
159
+ "stalux-random-list": "s4b",
160
+ "stalux-random-loading": "s4c",
161
+ "stalux-random-no-more": "s4d",
162
+ "stalux-random-refresh": "s4e",
163
+ "stalux-random-rotating": "s4f",
164
+ "stalux-random-title": "s4g",
165
+ "stalux-root": "s4h",
166
+ "stalux-sidebar": "s4i",
167
+ "stalux-sidebar-container": "s4j",
168
+ "stalux-sidebar-right": "s4k",
169
+ "stalux-site-header": "s4l",
170
170
  "stalux-site-nav": "s4m",
171
171
  "stalux-social": "s4n",
172
172
  "stalux-social-custom-icon": "s4o",
@@ -287,48 +287,49 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
287
287
  "--stalux-bg-color": "--s1n",
288
288
  "--stalux-bg-filter": "--s1o",
289
289
  "--stalux-nav-offset": "--s1p",
290
- "--stalux-text-shadow": "--s1q",
291
- "--surface-glass": "--s1r",
292
- "--surface-glass-shadow": "--s1s",
293
- "--tag-bg": "--s1t",
294
- "--tag-bg-hover": "--s1u",
295
- "--tl-line": "--s1v",
296
- "--tl-pad": "--s1w",
297
- "--transition-fast": "--s1x",
298
- "--transition-medium": "--s1y",
299
- "--transition-normal": "--s1z",
300
- "--transition-slow": "--s20",
301
- "--transition-slower": "--s21",
302
- "--transition-slowest": "--s22",
303
- "--un-rotate": "--s23",
304
- "--un-rotate-x": "--s24",
305
- "--un-rotate-y": "--s25",
306
- "--un-rotate-z": "--s26",
307
- "--un-scale-x": "--s27",
308
- "--un-scale-y": "--s28",
309
- "--un-scale-z": "--s29",
310
- "--un-skew-x": "--s2a",
311
- "--un-skew-y": "--s2b",
312
- "--un-translate-x": "--s2c",
313
- "--un-translate-y": "--s2d",
314
- "--un-translate-z": "--s2e",
315
- "--updated-bg": "--s2f",
316
- "--updated-border": "--s2g",
317
- "--white-05p": "--s2h",
318
- "--white-10p": "--s2i",
319
- "--white-15p": "--s2j",
320
- "--white-20p": "--s2k",
321
- "--white-30p": "--s2l",
322
- "--white-50p": "--s2m",
323
- "--white-60p": "--s2n",
324
- "--white-70p": "--s2o",
325
- "--white-75p": "--s2p",
326
- "--white-78p": "--s2q",
327
- "--white-80p": "--s2r",
328
- "--white-85p": "--s2s",
329
- "--white-90p": "--s2t",
330
- "--white-95p": "--s2u",
331
- "--white-full": "--s2v"
290
+ "--stalux-site-header-height": "--s1q",
291
+ "--stalux-text-shadow": "--s1r",
292
+ "--surface-glass": "--s1s",
293
+ "--surface-glass-shadow": "--s1t",
294
+ "--tag-bg": "--s1u",
295
+ "--tag-bg-hover": "--s1v",
296
+ "--tl-line": "--s1w",
297
+ "--tl-pad": "--s1x",
298
+ "--transition-fast": "--s1y",
299
+ "--transition-medium": "--s1z",
300
+ "--transition-normal": "--s20",
301
+ "--transition-slow": "--s21",
302
+ "--transition-slower": "--s22",
303
+ "--transition-slowest": "--s23",
304
+ "--un-rotate": "--s24",
305
+ "--un-rotate-x": "--s25",
306
+ "--un-rotate-y": "--s26",
307
+ "--un-rotate-z": "--s27",
308
+ "--un-scale-x": "--s28",
309
+ "--un-scale-y": "--s29",
310
+ "--un-scale-z": "--s2a",
311
+ "--un-skew-x": "--s2b",
312
+ "--un-skew-y": "--s2c",
313
+ "--un-translate-x": "--s2d",
314
+ "--un-translate-y": "--s2e",
315
+ "--un-translate-z": "--s2f",
316
+ "--updated-bg": "--s2g",
317
+ "--updated-border": "--s2h",
318
+ "--white-05p": "--s2i",
319
+ "--white-10p": "--s2j",
320
+ "--white-15p": "--s2k",
321
+ "--white-20p": "--s2l",
322
+ "--white-30p": "--s2m",
323
+ "--white-50p": "--s2n",
324
+ "--white-60p": "--s2o",
325
+ "--white-70p": "--s2p",
326
+ "--white-75p": "--s2q",
327
+ "--white-78p": "--s2r",
328
+ "--white-80p": "--s2s",
329
+ "--white-85p": "--s2t",
330
+ "--white-90p": "--s2u",
331
+ "--white-95p": "--s2v",
332
+ "--white-full": "--s2w"
332
333
  },
333
334
  "animations": {
334
335
  "fadeInUpComment": "k0",
@@ -52,7 +52,11 @@ const walineConfig = walineSection?.serverURL
52
52
  ---
53
53
 
54
54
  <div class="stalux-post-page">
55
- <div class="stalux-post-layout">
55
+ <div
56
+ class="stalux-post-layout"
57
+ transition:name="stalux-article-layout"
58
+ transition:animate="none"
59
+ >
56
60
  <!-- Left Sidebar -->
57
61
  <PostLeft author={author} authorAvatar={props.authorAvatar} />
58
62
 
@@ -71,7 +75,13 @@ const walineConfig = walineSection?.serverURL
71
75
  hasImage={props.hasImage}
72
76
  >
73
77
  <slot />
74
- <PostCC title={props.title} author={author} url={Astro.url.href} cc={props.cc} />
78
+ <PostCC
79
+ title={props.title}
80
+ author={author}
81
+ url={Astro.url.href}
82
+ cc={props.cc}
83
+ transitionName="stalux-article-license"
84
+ />
75
85
  <PostNavigation prev={props.prev} next={props.next} />
76
86
  {commentEnabled && walineConfig?.serverURL && (
77
87
  <WalineComment
@@ -2,6 +2,7 @@
2
2
  import { getCollection } from "astro:content";
3
3
  import Footer from "@components/stalux/layout/footer.astro";
4
4
  import Head from "@components/stalux/layout/head.astro";
5
+ import Navs from "@components/stalux/layout/navs.astro";
5
6
  import { getAccentColorStyle } from "@utils/accent-color";
6
7
  import {
7
8
  getAiDiscoveryData,
@@ -84,6 +85,15 @@ const routeAnimation = getRouteAnimation(routeKind);
84
85
  data-stalux-umami-id={headConfig?.umami?.id}
85
86
  data-stalux-umami-url={headConfig?.umami?.url}
86
87
  >
88
+ <header
89
+ class="stalux-site-header"
90
+ data-stalux-font-group="header"
91
+ transition:name="stalux-site-header"
92
+ transition:animate="none"
93
+ transition:persist="stalux-site-header"
94
+ >
95
+ <Navs />
96
+ </header>
87
97
  {llmPromote && (
88
98
  <section class="stalux-ai-prompt" data-stalux-ai-prompt aria-label="AI site context">
89
99
  <p>{llmPromote}</p>
@@ -105,8 +115,10 @@ const routeAnimation = getRouteAnimation(routeKind);
105
115
  <main
106
116
  class="stalux-main"
107
117
  data-stalux-route={routeKind}
108
- transition:name="stalux-page"
109
- transition:animate={routeAnimation}
118
+ transition:name={routeKind === "article" ? "stalux-article-page" : "stalux-page"}
119
+ transition:animate={routeKind === "home" || routeKind === "article"
120
+ ? "none"
121
+ : routeAnimation}
110
122
  >
111
123
  <slot />
112
124
  </main>