feat(opensource): 项目墙恢复居中,README 动效以自托管方式回归(5i T6/T8)
用户两项要求:开源页 Projects「依旧采取居中即可,不用左对齐」;README 迁移后
「很多图片元素还有一些 svg 基本上都摒弃了,可以在 readme 的基础上进行再创新,
保留那些动效的图,以及创新更多一些显示方式」。
T6 项目墙居中:`.repo` 从 align-items:stretch/text-align:left 改回 center/center,
`.repo-desc` 加 max-width:34em 防居中后长描述散架;语言点保持灰阶(与对齐无关)。
T8 动效回归(零运行时外链):
- 新增 geek/TypingLines.astro:自绘打字机(SSR 先出首行防闪白,JS 逐字打出/删除/
轮播,光标 CSS 闪烁,prefers-reduced-motion 与单条文案时保持静态,min-height
防抖,astro:page-load 重挂兼容软导航)——替代 readme-typing-svg 外链,且跟随主题
- 新增 scripts/build-perf/fetch-readme-assets.mjs:抓 stats/top-langs/streak/snake
到 public/github-assets/(10s 超时 + UA、透明底、亮暗两套站点色),产物入仓保证
离线可构建;单资产失败保留旧文件、无旧文件则该卡不渲染
- GithubProfile 新增「数据板」:灰阶 hairline 卡 + mono 小标题 + 2 列网格(移动单列),
与原生统计四格、语言占比、热力图组合成展示区;贡献蛇在暗色加 grayscale+invert
滤镜(SVG 内部是自带绿阶,直接贴暗底刺眼)
- build.sh 只做存在性守卫 + 缺资产 WARN,**不挂抓取**(发布链不被外网抖动拖累)
- 后台开源页新增 typing 字段(list,zh/en 双语,≤6 条,summary 取 zh)+ schema 声明
验证: 构建 51 页 exit 0;config-check 211 字段/66 hint 全过;E2E 46/46;
产物断言第三方外链零命中、本地引用 streak-light/streak-dark/snake;
Playwright 实测打字机文本 900ms 内变化(动效在跑)、streak naturalWidth=495、
snake=880、暗色自动切 streak-dark、项目墙 center/center、1280 与 390 均零横向溢出
备注: github-readme-stats 今日全局 503,stats/top-langs 两卡按契约跳过(原生四格与
语言条已覆盖同一信息);服务恢复后跑一次 fetch-readme-assets.mjs 即自动补上。
Spec: 2026-11-02_blog-5i-fonts-polish (T6/T8)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
23303bd39a
commit
0ff4ede91f
11 files changed
+608
-5
No files matched your search
@@ -39,6 +39,13 @@ pnpm astro build
|
|||||||
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
||||||
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
||||||
|
|
||||||
|
# README 动效资产(5i T8):**不在此跑抓取**(避免发布链被外网抖动拖累),只做存在性守卫 + 缺资产告警
|
||||||
|
if [ -f public/github-assets/manifest.json ]; then
|
||||||
|
echo "[build] github-assets 就绪:$(find public/github-assets -name '*.svg' | wc -l) 个自托管 SVG"
|
||||||
|
else
|
||||||
|
echo "[build] WARN public/github-assets/manifest.json 缺失——开源页数据板将只显示原生统计(可手动跑 node scripts/build-perf/fetch-readme-assets.mjs 补齐)"
|
||||||
|
fi
|
||||||
|
|
||||||
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
|
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
|
||||||
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
|
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
|
||||||
|
|
||||||
|
|||||||
@@ -535,6 +535,15 @@ collections:
|
|||||||
file: content/site/opensource.yaml
|
file: content/site/opensource.yaml
|
||||||
format: yaml
|
format: yaml
|
||||||
fields:
|
fields:
|
||||||
|
- label: 打字机动效文案(可选,≤6 条)
|
||||||
|
name: typing
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
summary: '{{fields.zh}}'
|
||||||
|
hint: '档案卡顶部逐字打出的几行文案,2–6 条最佳;留空用内置默认。中英各一行,切换语言同步'
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string }
|
||||||
|
- { label: English, name: en, widget: string }
|
||||||
- label: 叙述(链接前)
|
- label: 叙述(链接前)
|
||||||
name: blurb
|
name: blurb
|
||||||
widget: object
|
widget: object
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"assets": {
|
||||||
|
"streak-light.svg": {
|
||||||
|
"source": "https://streak-stats.demolab.com/?user=wait4xx&hide_border=true&background=00000000&ring=3454d1&fire=3454d1&currStreakLabel=3454d1&currStreakNum=33343a&sideNums=33343a&sideLabels=8b8f98&dates=8b8f98",
|
||||||
|
"fetchedAt": "2026-11-03T03:07:28.521Z",
|
||||||
|
"label": "连续贡献",
|
||||||
|
"theme": "light"
|
||||||
|
},
|
||||||
|
"streak-dark.svg": {
|
||||||
|
"source": "https://streak-stats.demolab.com/?user=wait4xx&hide_border=true&background=00000000&ring=3454d1&fire=3454d1&currStreakLabel=3454d1&currStreakNum=c9ccd4&sideNums=c9ccd4&sideLabels=9aa0aa&dates=9aa0aa",
|
||||||
|
"fetchedAt": "2026-11-03T03:07:28.521Z",
|
||||||
|
"label": "连续贡献",
|
||||||
|
"theme": "dark"
|
||||||
|
},
|
||||||
|
"snake.svg": {
|
||||||
|
"source": "https://raw.githubusercontent.com/wait4xx/wait4xx/output/github-contribution-grid-snake.svg",
|
||||||
|
"fetchedAt": "2026-11-03T03:07:28.521Z",
|
||||||
|
"label": "贡献蛇",
|
||||||
|
"theme": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="-16 -32 880 192" width="880" height="192" xmlns="http://www.w3.org/2000/svg"><desc>Generated with https://github.com/Platane/snk</desc><style>:root{--cb:#1b1f230a;--cs:purple;--ce:#ebedf0;--c0:#ebedf0;--c1:#9be9a8;--c2:#40c463;--c3:#30a14e;--c4:#216e39}.c{shape-rendering:geometricPrecision;fill:var(--ce);stroke-width:1px;stroke:var(--cb);animation:none 35300ms linear infinite;width:12px;height:12px}@keyframes c0{4.52%{fill:var(--c1)}4.54%,100%{fill:var(--ce)}}.c.c0{fill:var(--c1);animation-name:c0}@keyframes c1{66.85%{fill:var(--c2)}66.87%,100%{fill:var(--ce)}}.c.c1{fill:var(--c2);animation-name:c1}@keyframes c2{3.67%{fill:var(--c1)}3.69%,100%{fill:var(--ce)}}.c.c2{fill:var(--c1);animation-name:c2}@keyframes c3{1.97%{fill:var(--c1)}1.99%,100%{fill:var(--ce)}}.c.c3{fill:var(--c1);animation-name:c3}@keyframes c4{2.82%{fill:var(--c1)}2.84%,100%{fill:var(--ce)}}.c.c4{fill:var(--c1);animation-name:c4}@keyframes c5{3.39%{fill:var(--c1)}3.41%,100%{fill:var(--ce)}}.c.c5{fill:var(--c1);animation-name:c5}@keyframes c6{2.26%{fill:var(--c1)}2.28%,100%{fill:var(--ce)}}.c.c6{fill:var(--c1);animation-name:c6}@keyframes c7{10.19%{fill:var(--c1)}10.21%,100%{fill:var(--ce)}}.c.c7{fill:var(--c1);animation-name:c7}@keyframes c8{9.06%{fill:var(--c1)}9.08%,100%{fill:var(--ce)}}.c.c8{fill:var(--c1);animation-name:c8}@keyframes c9{41.63%{fill:var(--c1)}41.65%,100%{fill:var(--ce)}}.c.c9{fill:var(--c1);animation-name:c9}@keyframes ca{12.74%{fill:var(--c1)}12.76%,100%{fill:var(--ce)}}.c.ca{fill:var(--c1);animation-name:ca}@keyframes cb{13.59%{fill:var(--c1)}13.61%,100%{fill:var(--ce)}}.c.cb{fill:var(--c1);animation-name:cb}@keyframes cc{14.15%{fill:var(--c1)}14.17%,100%{fill:var(--ce)}}.c.cc{fill:var(--c1);animation-name:cc}@keyframes cd{14.44%{fill:var(--c1)}14.46%,100%{fill:var(--ce)}}.c.cd{fill:var(--c1);animation-name:cd}@keyframes ce{21.8%{fill:var(--c1)}21.82%,100%{fill:var(--ce)}}.c.ce{fill:var(--c1);animation-name:ce}@keyframes cf{19.82%{fill:var(--c1)}19.84%,100%{fill:var(--ce)}}.c.cf{fill:var(--c1);animation-name:cf}@keyframes cg{20.67%{fill:var(--c1)}20.69%,100%{fill:var(--ce)}}.c.cg{fill:var(--c1);animation-name:cg}@keyframes ch{14.72%{fill:var(--c1)}14.74%,100%{fill:var(--ce)}}.c.ch{fill:var(--c1);animation-name:ch}@keyframes ci{21.52%{fill:var(--c1)}21.54%,100%{fill:var(--ce)}}.c.ci{fill:var(--c1);animation-name:ci}@keyframes cj{19.25%{fill:var(--c1)}19.27%,100%{fill:var(--ce)}}.c.cj{fill:var(--c1);animation-name:cj}@keyframes ck{15%{fill:var(--c1)}15.02%,100%{fill:var(--ce)}}.c.ck{fill:var(--c1);animation-name:ck}@keyframes cl{15.29%{fill:var(--c1)}15.31%,100%{fill:var(--ce)}}.c.cl{fill:var(--c1);animation-name:cl}@keyframes cm{44.18%{fill:var(--c2)}44.2%,100%{fill:var(--ce)}}.c.cm{fill:var(--c2);animation-name:cm}@keyframes cn{18.4%{fill:var(--c1)}18.42%,100%{fill:var(--ce)}}.c.cn{fill:var(--c1);animation-name:cn}@keyframes co{18.69%{fill:var(--c1)}18.71%,100%{fill:var(--ce)}}.c.co{fill:var(--c1);animation-name:co}@keyframes cp{16.14%{fill:var(--c1)}16.16%,100%{fill:var(--ce)}}.c.cp{fill:var(--c1);animation-name:cp}@keyframes cq{15.85%{fill:var(--c1)}15.87%,100%{fill:var(--ce)}}.c.cq{fill:var(--c1);animation-name:cq}@keyframes cr{23.5%{fill:var(--c1)}23.52%,100%{fill:var(--ce)}}.c.cr{fill:var(--c1);animation-name:cr}@keyframes cs{18.12%{fill:var(--c1)}18.14%,100%{fill:var(--ce)}}.c.cs{fill:var(--c1);animation-name:cs}@keyframes ct{89.79%{fill:var(--c4)}89.81%,100%{fill:var(--ce)}}.c.ct{fill:var(--c4);animation-name:ct}@keyframes cu{16.99%{fill:var(--c1)}17.01%,100%{fill:var(--ce)}}.c.cu{fill:var(--c1);animation-name:cu}@keyframes cv{76.19%{fill:var(--c4)}76.21%,100%{fill:var(--ce)}}.c.cv{fill:var(--c4);animation-name:cv}@keyframes cw{47.02%{fill:var(--c2)}47.04%,100%{fill:var(--ce)}}.c.cw{fill:var(--c2);animation-name:cw}@keyframes cx{35.4%{fill:var(--c1)}35.42%,100%{fill:var(--ce)}}.c.cx{fill:var(--c1);animation-name:cx}@keyframes cy{35.12%{fill:var(--c1)}35.14%,100%{fill:var(--ce)}}.c.cy{fill:var(--c1);animation-name:cy}@keyframes cz{26.34%{fill:var(--c1)}26.36%,100%{fill:var(--ce)}}.c.cz{fill:var(--c1);animation-name:cz}@keyframes c10{33.7%{fill:var(--c1)}33.72%,100%{fill:var(--ce)}}.c.c10{fill:var(--c1);animation-name:c10}@keyframes c11{33.98%{fill:var(--c1)}34%,100%{fill:var(--ce)}}.c.c11{fill:var(--c1);animation-name:c11}@keyframes c12{50.41%{fill:var(--c2)}50.43%,100%{fill:var(--ce)}}.c.c12{fill:var(--c2);animation-name:c12}@keyframes c13{78.46%{fill:var(--c4)}78.48%,100%{fill:var(--ce)}}.c.c13{fill:var(--c4);animation-name:c13}@keyframes c14{49.28%{fill:var(--c2)}49.3%,100%{fill:var(--ce)}}.c.c14{fill:var(--c2);animation-name:c14}@keyframes c15{27.47%{fill:var(--c1)}27.49%,100%{fill:var(--ce)}}.c.c15{fill:var(--c1);animation-name:c15}@keyframes c16{80.44%{fill:var(--c4)}80.46%,100%{fill:var(--ce)}}.c.c16{fill:var(--c4);animation-name:c16}@keyframes c17{27.75%{fill:var(--c1)}27.77%,100%{fill:var(--ce)}}.c.c17{fill:var(--c1);animation-name:c17}@keyframes c18{32.28%{fill:var(--c1)}32.3%,100%{fill:vLine truncated
|
||||||
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,111 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
|
||||||
|
style='isolation: isolate' viewBox='0 0 495 195' width='495px' height='195px' direction='ltr'>
|
||||||
|
<style>
|
||||||
|
@keyframes currstreak {
|
||||||
|
0% { font-size: 3px; opacity: 0.2; }
|
||||||
|
80% { font-size: 34px; opacity: 1; }
|
||||||
|
100% { font-size: 28px; opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes fadein {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='outer_rectangle'>
|
||||||
|
<rect width='495' height='195' rx='4.5'/>
|
||||||
|
</clipPath>
|
||||||
|
<mask id='mask_out_ring_behind_fire'>
|
||||||
|
<rect width='495' height='195' fill='white'/>
|
||||||
|
<ellipse id='mask-ellipse' cx='247.5' cy='32' rx='13' ry='18' fill='black'/>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#outer_rectangle)'>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<rect stroke='#000000' stroke-opacity='0' fill='#000000' fill-opacity='0' rx='4.5' x='0.5' y='0.5' width='494' height='194'/>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<line x1='165' y1='28' x2='165' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#E4E2E2' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
|
||||||
|
<line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#E4E2E2' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Total Contributions big number -->
|
||||||
|
<g transform='translate(82.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#c9ccd4' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
|
||||||
|
344
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Total Contributions label -->
|
||||||
|
<g transform='translate(82.5, 84)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#9aa0aa' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>
|
||||||
|
Total Contributions
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Total Contributions range -->
|
||||||
|
<g transform='translate(82.5, 114)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#9aa0aa' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>
|
||||||
|
May 24, 2019 - Present
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Current Streak label -->
|
||||||
|
<g transform='translate(247.5, 108)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#3454d1' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
|
||||||
|
Current Streak
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Current Streak range -->
|
||||||
|
<g transform='translate(247.5, 145)'>
|
||||||
|
<text x='0' y='21' stroke-width='0' text-anchor='middle' fill='#9aa0aa' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
|
||||||
|
Nov 3
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Ring around number -->
|
||||||
|
<g mask='url(#mask_out_ring_behind_fire)'>
|
||||||
|
<circle cx='247.5' cy='71' r='40' fill='none' stroke='#3454d1' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
|
||||||
|
</g>
|
||||||
|
<!-- Fire icon -->
|
||||||
|
<g transform='translate(247.5, 19.5)' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
|
||||||
|
<path d='M -12 -0.5 L 15 -0.5 L 15 23.5 L -12 23.5 L -12 -0.5 Z' fill='none'/>
|
||||||
|
<path d='M 1.5 0.67 C 1.5 0.67 2.24 3.32 2.24 5.47 C 2.24 7.53 0.89 9.2 -1.17 9.2 C -3.23 9.2 -4.79 7.53 -4.79 5.47 L -4.76 5.11 C -6.78 7.51 -8 10.62 -8 13.99 C -8 18.41 -4.42 22 0 22 C 4.42 22 8 18.41 8 13.99 C 8 8.6 5.41 3.79 1.5 0.67 Z M -0.29 19 C -2.07 19 -3.51 17.6 -3.51 15.86 C -3.51 14.24 -2.46 13.1 -0.7 12.74 C 1.07 12.38 2.9 11.53 3.92 10.16 C 4.31 11.45 4.51 12.81 4.51 14.2 C 4.51 16.85 2.36 19 -0.29 19 Z' fill='#3454d1' stroke-opacity='0'/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Current Streak big number -->
|
||||||
|
<g transform='translate(247.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#c9ccd4' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>
|
||||||
|
0
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Longest Streak big number -->
|
||||||
|
<g transform='translate(412.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#c9ccd4' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>
|
||||||
|
6
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Longest Streak label -->
|
||||||
|
<g transform='translate(412.5, 84)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#9aa0aa' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>
|
||||||
|
Longest Streak
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Longest Streak range -->
|
||||||
|
<g transform='translate(412.5, 114)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#9aa0aa' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>
|
||||||
|
Oct 19 - Oct 24
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,111 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
|
||||||
|
style='isolation: isolate' viewBox='0 0 495 195' width='495px' height='195px' direction='ltr'>
|
||||||
|
<style>
|
||||||
|
@keyframes currstreak {
|
||||||
|
0% { font-size: 3px; opacity: 0.2; }
|
||||||
|
80% { font-size: 34px; opacity: 1; }
|
||||||
|
100% { font-size: 28px; opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes fadein {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='outer_rectangle'>
|
||||||
|
<rect width='495' height='195' rx='4.5'/>
|
||||||
|
</clipPath>
|
||||||
|
<mask id='mask_out_ring_behind_fire'>
|
||||||
|
<rect width='495' height='195' fill='white'/>
|
||||||
|
<ellipse id='mask-ellipse' cx='247.5' cy='32' rx='13' ry='18' fill='black'/>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#outer_rectangle)'>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<rect stroke='#000000' stroke-opacity='0' fill='#000000' fill-opacity='0' rx='4.5' x='0.5' y='0.5' width='494' height='194'/>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<line x1='165' y1='28' x2='165' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#E4E2E2' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
|
||||||
|
<line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#E4E2E2' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Total Contributions big number -->
|
||||||
|
<g transform='translate(82.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#33343a' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
|
||||||
|
344
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Total Contributions label -->
|
||||||
|
<g transform='translate(82.5, 84)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#8b8f98' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>
|
||||||
|
Total Contributions
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Total Contributions range -->
|
||||||
|
<g transform='translate(82.5, 114)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#8b8f98' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>
|
||||||
|
May 24, 2019 - Present
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Current Streak label -->
|
||||||
|
<g transform='translate(247.5, 108)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#3454d1' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
|
||||||
|
Current Streak
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Current Streak range -->
|
||||||
|
<g transform='translate(247.5, 145)'>
|
||||||
|
<text x='0' y='21' stroke-width='0' text-anchor='middle' fill='#8b8f98' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
|
||||||
|
Nov 3
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Ring around number -->
|
||||||
|
<g mask='url(#mask_out_ring_behind_fire)'>
|
||||||
|
<circle cx='247.5' cy='71' r='40' fill='none' stroke='#3454d1' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
|
||||||
|
</g>
|
||||||
|
<!-- Fire icon -->
|
||||||
|
<g transform='translate(247.5, 19.5)' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
|
||||||
|
<path d='M -12 -0.5 L 15 -0.5 L 15 23.5 L -12 23.5 L -12 -0.5 Z' fill='none'/>
|
||||||
|
<path d='M 1.5 0.67 C 1.5 0.67 2.24 3.32 2.24 5.47 C 2.24 7.53 0.89 9.2 -1.17 9.2 C -3.23 9.2 -4.79 7.53 -4.79 5.47 L -4.76 5.11 C -6.78 7.51 -8 10.62 -8 13.99 C -8 18.41 -4.42 22 0 22 C 4.42 22 8 18.41 8 13.99 C 8 8.6 5.41 3.79 1.5 0.67 Z M -0.29 19 C -2.07 19 -3.51 17.6 -3.51 15.86 C -3.51 14.24 -2.46 13.1 -0.7 12.74 C 1.07 12.38 2.9 11.53 3.92 10.16 C 4.31 11.45 4.51 12.81 4.51 14.2 C 4.51 16.85 2.36 19 -0.29 19 Z' fill='#3454d1' stroke-opacity='0'/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Current Streak big number -->
|
||||||
|
<g transform='translate(247.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#33343a' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>
|
||||||
|
0
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
<g style='isolation: isolate'>
|
||||||
|
<!-- Longest Streak big number -->
|
||||||
|
<g transform='translate(412.5, 48)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#33343a' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>
|
||||||
|
6
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Longest Streak label -->
|
||||||
|
<g transform='translate(412.5, 84)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#8b8f98' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>
|
||||||
|
Longest Streak
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Longest Streak range -->
|
||||||
|
<g transform='translate(412.5, 114)'>
|
||||||
|
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#8b8f98' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>
|
||||||
|
Oct 19 - Oct 24
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// README 动效资产抓取器(5i T8):把只能靠外部生成的 SVG 数据卡抓成本地静态文件
|
||||||
|
// 设计取舍:
|
||||||
|
// · 产物入仓(public/github-assets/)→ 构建确定性与离线可构建优先,不在 CI 里跑本脚本
|
||||||
|
// · 单资产失败保留旧文件;无旧文件则跳过(组件侧按存在性决定是否渲染该块)
|
||||||
|
// · 配色按站点 tokens 近似:透明底 + 文字 #33343a(亮)/#c9ccd4(暗) + 线条 #3454d1
|
||||||
|
// · manifest.json 记录来源 URL 与抓取时间(可追溯,不含任何凭据)
|
||||||
|
// 用法:node scripts/build-perf/fetch-readme-assets.mjs [--force]
|
||||||
|
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const OUT_DIR = 'public/github-assets';
|
||||||
|
const LOGIN = 'wait4xx';
|
||||||
|
const TIMEOUT_MS = 10_000;
|
||||||
|
const UA = 'wait4x-blog-assets/1.0 (+https://wait4x.online)';
|
||||||
|
|
||||||
|
// 站点 tokens 近似值(与灰阶纪律一致:底透明,线条单一强调色)
|
||||||
|
const ACCENT = '3454d1';
|
||||||
|
const INK_LIGHT = '33343a';
|
||||||
|
const INK_DARK = 'c9ccd4';
|
||||||
|
const MUTED_LIGHT = '8b8f98';
|
||||||
|
const MUTED_DARK = '9aa0aa';
|
||||||
|
|
||||||
|
/** 同一张卡按明暗两套参数各存一份(组件内用 CSS 切换,零运行时请求) */
|
||||||
|
const ASSETS = [
|
||||||
|
{
|
||||||
|
name: 'stats',
|
||||||
|
url: (theme) =>
|
||||||
|
`https://github-readme-stats.vercel.app/api?username=${LOGIN}` +
|
||||||
|
'&show_icons=true&hide_border=true&include_all_commits=true&count_private=true&rank_icon=github' +
|
||||||
|
`&bg_color=00000000&title_color=${ACCENT}&icon_color=${ACCENT}` +
|
||||||
|
`&text_color=${theme === 'dark' ? INK_DARK : INK_LIGHT}`,
|
||||||
|
label: 'GitHub 统计',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'top-langs',
|
||||||
|
url: (theme) =>
|
||||||
|
`https://github-readme-stats.vercel.app/api/top-langs/?username=${LOGIN}` +
|
||||||
|
'&layout=compact&hide_border=true&langs_count=6' +
|
||||||
|
`&bg_color=00000000&title_color=${ACCENT}` +
|
||||||
|
`&text_color=${theme === 'dark' ? INK_DARK : INK_LIGHT}`,
|
||||||
|
label: '语言分布',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'streak',
|
||||||
|
url: (theme) =>
|
||||||
|
`https://streak-stats.demolab.com/?user=${LOGIN}&hide_border=true` +
|
||||||
|
'&background=00000000' +
|
||||||
|
`&ring=${ACCENT}&fire=${ACCENT}&currStreakLabel=${ACCENT}` +
|
||||||
|
`&currStreakNum=${theme === 'dark' ? INK_DARK : INK_LIGHT}` +
|
||||||
|
`&sideNums=${theme === 'dark' ? INK_DARK : INK_LIGHT}` +
|
||||||
|
`&sideLabels=${theme === 'dark' ? MUTED_DARK : MUTED_LIGHT}` +
|
||||||
|
`&dates=${theme === 'dark' ? MUTED_DARK : MUTED_LIGHT}`,
|
||||||
|
label: '连续贡献',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 贡献蛇:GitHub Action 生成,配色由 action 侧决定,无法传参 → 只抓一份
|
||||||
|
name: 'snake',
|
||||||
|
url: () => `https://raw.githubusercontent.com/${LOGIN}/${LOGIN}/output/github-contribution-grid-snake.svg`,
|
||||||
|
label: '贡献蛇',
|
||||||
|
single: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 10s 超时抓取;返回 SVG 文本或 null */
|
||||||
|
async function grab(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { 'User-Agent': UA, Accept: 'image/svg+xml,image/*;q=0.8,*/*;q=0.5' },
|
||||||
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn(`[assets] HTTP ${res.status}: ${url.slice(0, 90)}…`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
if (!text.includes('<svg')) {
|
||||||
|
console.warn(`[assets] 非 SVG 响应,已跳过: ${url.slice(0, 90)}…`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[assets] 抓取失败(${err.name === 'TimeoutError' ? '超时' : err.message}): ${url.slice(0, 90)}…`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const force = process.argv.includes('--force');
|
||||||
|
mkdirSync(OUT_DIR, { recursive: true });
|
||||||
|
const manifestPath = join(OUT_DIR, 'manifest.json');
|
||||||
|
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : { assets: {} };
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
let ok = 0;
|
||||||
|
let kept = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const asset of ASSETS) {
|
||||||
|
const themes = asset.single ? [null] : ['light', 'dark'];
|
||||||
|
for (const theme of themes) {
|
||||||
|
const fileName = theme ? `${asset.name}-${theme}.svg` : `${asset.name}.svg`;
|
||||||
|
const filePath = join(OUT_DIR, fileName);
|
||||||
|
const url = asset.url(theme);
|
||||||
|
const svg = await grab(url);
|
||||||
|
if (svg) {
|
||||||
|
writeFileSync(filePath, svg, 'utf8');
|
||||||
|
manifest.assets[fileName] = { source: url, fetchedAt: now, label: asset.label, theme: theme ?? 'any' };
|
||||||
|
console.log(`[assets] ✓ ${fileName} (${Math.round(svg.length / 1024)}KB)`);
|
||||||
|
ok += 1;
|
||||||
|
} else if (existsSync(filePath)) {
|
||||||
|
console.log(`[assets] ↺ ${fileName} 抓取失败,保留旧文件`);
|
||||||
|
kept += 1;
|
||||||
|
} else {
|
||||||
|
console.warn(`[assets] ⊘ ${fileName} 抓取失败且无旧文件 → 构建时将跳过该块`);
|
||||||
|
skipped += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
||||||
|
console.log(`[assets] 完成:新增/更新 ${ok},保留旧文件 ${kept},跳过 ${skipped};manifest → ${manifestPath}`);
|
||||||
|
process.exit(0);
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
// 0600 文件,缺位或失败即隐藏该数字)④src/lib/projects.ts 的仓库列表(语言占比与星标汇总)
|
// 0600 文件,缺位或失败即隐藏该数字)④src/lib/projects.ts 的仓库列表(语言占比与星标汇总)
|
||||||
// 降级纪律:任何一项取数失败只隐藏该项,绝不炸构建、不出现空图片框
|
// 降级纪律:任何一项取数失败只隐藏该项,绝不炸构建、不出现空图片框
|
||||||
import { Image } from 'astro:assets';
|
import { Image } from 'astro:assets';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import TypingLines from './TypingLines.astro';
|
||||||
import { homeAvatar, homeSocials, openSourceCopy } from '../../lib/site-content';
|
import { homeAvatar, homeSocials, openSourceCopy } from '../../lib/site-content';
|
||||||
import { githubUserStats, loadProjects } from '../../lib/projects';
|
import { githubUserStats, loadProjects } from '../../lib/projects';
|
||||||
import { iconPath } from '../../lib/icons';
|
import { iconPath } from '../../lib/icons';
|
||||||
@@ -49,6 +51,40 @@ try {
|
|||||||
// 只给第一根上强调色会让并列者显得"落后",故并列者一并着色
|
// 只给第一根上强调色会让并列者显得"落后",故并列者一并着色
|
||||||
const maxPct = langs.reduce((m, l) => Math.max(m, l.pct), 0);
|
const maxPct = langs.reduce((m, l) => Math.max(m, l.pct), 0);
|
||||||
const stats = await githubUserStats();
|
const stats = await githubUserStats();
|
||||||
|
|
||||||
|
// 打字机动效文案:content/site/opensource.yaml 的 typing[](双语,后台可改)
|
||||||
|
// 注:schema 由 orchestrator 统一扩字段;未声明时这里的类型断言取不到值 → 走内置默认
|
||||||
|
type TypingEntry = { zh?: string; en?: string };
|
||||||
|
const typingRaw = (copy as unknown as { typing?: TypingEntry[] }).typing ?? [];
|
||||||
|
const typingLines = typingRaw
|
||||||
|
.map((t) => (locale === 'zh' ? t.zh : (t.en ?? t.zh)) ?? '')
|
||||||
|
.filter(Boolean);
|
||||||
|
const DEFAULT_TYPING = {
|
||||||
|
zh: ['气象出身,写代码为乐', '自托管一切能自托管的东西', '拥抱开源,热爱开源'],
|
||||||
|
en: ['Trained in meteorology, coding for fun', 'Self-hosting whatever can be self-hosted', 'Embrace open source, love open source'],
|
||||||
|
}[locale];
|
||||||
|
const lines = typingLines.length ? typingLines : DEFAULT_TYPING;
|
||||||
|
|
||||||
|
// 自托管 SVG 数据卡(public/github-assets/,由 scripts/build-perf/fetch-readme-assets.mjs 产出入仓)
|
||||||
|
// 缺文件即跳过该卡(异步抓取失败不影响构建),明暗各一份时用 CSS 切换
|
||||||
|
const assetPath = (name: string) => `/github-assets/${name}`;
|
||||||
|
const hasAsset = (name: string) => existsSync(`public/github-assets/${name}`);
|
||||||
|
const boardCards: { label: string; light: string; dark?: string }[] = [];
|
||||||
|
for (const [key, label] of [
|
||||||
|
['stats', locale === 'zh' ? '统计' : 'Stats'],
|
||||||
|
['top-langs', locale === 'zh' ? '语言分布' : 'Top languages'],
|
||||||
|
['streak', locale === 'zh' ? '连续贡献' : 'Streak'],
|
||||||
|
] as const) {
|
||||||
|
if (!hasAsset(`${key}-light.svg`)) continue;
|
||||||
|
boardCards.push({
|
||||||
|
label,
|
||||||
|
light: assetPath(`${key}-light.svg`),
|
||||||
|
dark: hasAsset(`${key}-dark.svg`) ? assetPath(`${key}-dark.svg`) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const snakeCard = hasAsset('snake.svg')
|
||||||
|
? { label: locale === 'zh' ? '贡献蛇' : 'Contribution snake', src: assetPath('snake.svg') }
|
||||||
|
: null;
|
||||||
const L = {
|
const L = {
|
||||||
zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' },
|
zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' },
|
||||||
en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' },
|
en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' },
|
||||||
@@ -66,6 +102,7 @@ const L = {
|
|||||||
<p class="pf-name">
|
<p class="pf-name">
|
||||||
wait4x<span class="pf-handle mono">@wait4xx</span>
|
wait4x<span class="pf-handle mono">@wait4xx</span>
|
||||||
</p>
|
</p>
|
||||||
|
<TypingLines lines={lines} />
|
||||||
<p class="pf-bio">
|
<p class="pf-bio">
|
||||||
{copy.blurb}<a class="pf-link" href={copy.repoUrl}>{copy.repoLabel}</a>{' '}{copy.blurbAfter.trim()}
|
{copy.blurb}<a class="pf-link" href={copy.repoUrl}>{copy.repoLabel}</a>{' '}{copy.blurbAfter.trim()}
|
||||||
</p>
|
</p>
|
||||||
@@ -123,6 +160,28 @@ const L = {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{(boardCards.length > 0 || snakeCard) && (
|
||||||
|
<div class="pf-board">
|
||||||
|
<p class="pf-board-title mono">{locale === 'zh' ? '数据板' : 'Data board'}</p>
|
||||||
|
<div class="pf-board-grid">
|
||||||
|
{boardCards.map((card) => (
|
||||||
|
<figure class="pf-card">
|
||||||
|
<img class="pf-card-img only-light" src={card.light} alt={card.label} loading="lazy" decoding="async" />
|
||||||
|
{card.dark && (
|
||||||
|
<img class="pf-card-img only-dark" src={card.dark} alt={card.label} loading="lazy" decoding="async" />
|
||||||
|
)}
|
||||||
|
<figcaption class="mono">{card.label}</figcaption>
|
||||||
|
</figure>
|
||||||
|
))}
|
||||||
|
{snakeCard && (
|
||||||
|
<figure class="pf-card pf-card-wide pf-card-snake">
|
||||||
|
<img class="pf-card-img" src={snakeCard.src} alt={snakeCard.label} loading="lazy" decoding="async" />
|
||||||
|
<figcaption class="mono">{snakeCard.label}</figcaption>
|
||||||
|
</figure>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -253,8 +312,55 @@ const L = {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.pf-board {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.pf-board-title {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
.pf-board-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
.pf-card {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.pf-card-wide { grid-column: 1 / -1; }
|
||||||
|
.pf-card-img { max-width: 100%; height: auto; display: block; }
|
||||||
|
.pf-card figcaption {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
/* 明暗两版自托管 SVG:默认只显示亮色,暗色主题切到暗色版 */
|
||||||
|
/* 贡献蛇:SVG 内部用自带 CSS 变量(绿阶 + #ebedf0 空格),<img> 无法改写内部配色 →
|
||||||
|
滤镜归一到站点灰阶;暗色下反相,避免浅灰格子贴在深底上刺眼(想去掉绿→灰只改这一行) */
|
||||||
|
.pf-card-snake .pf-card-img { filter: grayscale(1) contrast(1.03); }
|
||||||
|
:global([data-theme='dark']) .pf-card-snake .pf-card-img {
|
||||||
|
filter: grayscale(1) invert(1) contrast(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-dark { display: none; }
|
||||||
|
:global([data-theme='dark']) .only-light { display: none; }
|
||||||
|
:global([data-theme='dark']) .only-dark { display: block; }
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.pf-lang { grid-template-columns: 5.5rem 1fr 2.6rem; }
|
.pf-lang { grid-template-columns: 5.5rem 1fr 2.6rem; }
|
||||||
.pf-stats { gap: var(--space-3); }
|
.pf-stats { gap: var(--space-3); }
|
||||||
|
.pf-board-grid { grid-template-columns: minmax(0, 1fr); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -73,10 +73,9 @@ try {
|
|||||||
.repo {
|
.repo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* 显式接管交叉轴:全局 .tap 是 inline-flex + align-items:center,
|
/* 用户拍板(5i T6):Projects 卡片内容居中,不用左对齐 */
|
||||||
列向 flex 下会把标题/元信息行水平居中(视觉终审 P1,卡片左缘参差) */
|
align-items: center;
|
||||||
align-items: stretch;
|
text-align: center;
|
||||||
text-align: left;
|
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -86,7 +85,12 @@ try {
|
|||||||
}
|
}
|
||||||
.repo:hover { border-color: var(--accent); }
|
.repo:hover { border-color: var(--accent); }
|
||||||
.repo-name { font-family: var(--font-mono); font-size: 14px; color: var(--ink-1); }
|
.repo-name { font-family: var(--font-mono); font-size: 14px; color: var(--ink-1); }
|
||||||
.repo-desc { font-size: 13px; color: var(--ink-3); }
|
.repo-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
/* 居中后长描述限行宽,避免整行摊平散架 */
|
||||||
|
max-width: 34em;
|
||||||
|
}
|
||||||
.repo-meta { display: flex; gap: var(--space-3); font-size: 12px; color: var(--ink-3); margin-top: 4px; }
|
.repo-meta { display: flex; gap: var(--space-3); font-size: 12px; color: var(--ink-3); margin-top: 4px; }
|
||||||
.lang { display: inline-flex; align-items: center; gap: 4px; }
|
.lang { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.dot {
|
.dot {
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
// 自绘打字机动效(5i T8):替代外链 readme-typing-svg
|
||||||
|
// 纯 CSS + 极少 JS:SSR 先渲染第一行(无 JS / reduced-motion 时就是静态首行,不闪不跳),
|
||||||
|
// JS 接管后循环「逐字打出 → 停留 → 逐字删除 → 下一行」;光标闪烁走 CSS keyframes。
|
||||||
|
interface Props {
|
||||||
|
/** 已本地化的文案行(保持 ≤6 行) */
|
||||||
|
lines: string[];
|
||||||
|
/** 每字毫秒 */
|
||||||
|
speed?: number;
|
||||||
|
}
|
||||||
|
const { lines, speed = 55 } = Astro.props;
|
||||||
|
const safe = lines.map((l) => l.trim()).filter(Boolean).slice(0, 6);
|
||||||
|
const first = safe[0] ?? '';
|
||||||
|
---
|
||||||
|
|
||||||
|
{safe.length > 0 && (
|
||||||
|
<p
|
||||||
|
class="typing"
|
||||||
|
data-lines={JSON.stringify(safe)}
|
||||||
|
data-speed={speed}
|
||||||
|
aria-label={safe.join(';')}
|
||||||
|
>
|
||||||
|
<span class="typing-text" aria-hidden="true">{first}</span>
|
||||||
|
<span class="typing-caret" aria-hidden="true"></span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 每次软导航重挂:ClientRouter 下模块脚本只跑一次 → 用 astro:page-load 统一初始化
|
||||||
|
const reducedMotion = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
function initTyping() {
|
||||||
|
document.querySelectorAll<HTMLElement>('.typing').forEach((el) => {
|
||||||
|
if (el.dataset.typingOn === '1') return;
|
||||||
|
el.dataset.typingOn = '1';
|
||||||
|
const lines: string[] = JSON.parse(el.dataset.lines ?? '[]');
|
||||||
|
const text = el.querySelector<HTMLElement>('.typing-text');
|
||||||
|
if (!text || lines.length < 2 || reducedMotion()) return; // 单行/减动效:保留 SSR 首行
|
||||||
|
|
||||||
|
const speed = Number(el.dataset.speed ?? 55);
|
||||||
|
let line = 0;
|
||||||
|
let char = lines[0].length;
|
||||||
|
let erasing = true;
|
||||||
|
let timer: number | undefined;
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
const current = lines[line % lines.length];
|
||||||
|
if (erasing) {
|
||||||
|
char -= 1;
|
||||||
|
text.textContent = current.slice(0, Math.max(char, 0));
|
||||||
|
if (char <= 0) {
|
||||||
|
erasing = false;
|
||||||
|
line += 1;
|
||||||
|
timer = window.setTimeout(step, 260);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timer = window.setTimeout(step, Math.max(18, speed / 2));
|
||||||
|
} else {
|
||||||
|
char += 1;
|
||||||
|
text.textContent = lines[line % lines.length].slice(0, char);
|
||||||
|
if (char >= lines[line % lines.length].length) {
|
||||||
|
erasing = true;
|
||||||
|
timer = window.setTimeout(step, 1800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timer = window.setTimeout(step, speed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 首行打完再停留一下开始删除
|
||||||
|
timer = window.setTimeout(step, 1800);
|
||||||
|
el.addEventListener('astro:before-swap', () => window.clearTimeout(timer), { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initTyping();
|
||||||
|
document.addEventListener('astro:page-load', initTyping);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.typing {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--ink-3);
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
min-height: 1.6em; /* 固定行高,打字期间不推挤下方内容 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
.typing-text { white-space: pre-wrap; }
|
||||||
|
.typing-caret {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 1em;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: caret-blink 1.05s steps(1) infinite;
|
||||||
|
}
|
||||||
|
@keyframes caret-blink {
|
||||||
|
0%, 49% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.typing-caret { animation: none; opacity: 0.6; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -292,6 +292,11 @@ export const collections = {
|
|||||||
perSource: z.enum(['both', 'github', 'gitea']).optional(),
|
perSource: z.enum(['both', 'github', 'gitea']).optional(),
|
||||||
/** 开篇文案来源:off(默认,用本文件 blurb)/ github-readme(构建期取 profile 仓 README) */
|
/** 开篇文案来源:off(默认,用本文件 blurb)/ github-readme(构建期取 profile 仓 README) */
|
||||||
intro_from: z.enum(['off', 'github-readme']).optional(),
|
intro_from: z.enum(['off', 'github-readme']).optional(),
|
||||||
|
/**
|
||||||
|
* 档案卡顶部的打字机动效文案(自绘 CSS/JS,非第三方 SVG)。
|
||||||
|
* 越界(>6 条)由访问器截断;全空则组件用内置默认三行。
|
||||||
|
*/
|
||||||
|
typing: z.array(bilingual).optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
// slug 即文件名(glob loader 默认按文件名生成 id)
|
// slug 即文件名(glob loader 默认按文件名生成 id)
|
||||||
|
|||||||
Reference in new issue
Block a user