From 11a50b4d2365f00d32bf344cfe54f616cf56b2ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 01:10:00 +0000 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=E5=AE=98=E7=BD=91=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=B7=B1=E8=89=B2=E7=A7=91=E6=8A=80=E9=A3=8E=E5=B9=B6?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=20GSAP=20=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配色改为近黑底 + 紫青渐变,默认深色,浅色仍可切换;token 全部通过 WCAG AA 检查 - 样式重做:玻璃质感卡片、首屏极光与网格背景、渐变标题、终端窗口栏、鼠标跟随高光 - 新增 js/motion.js:Lenis 平滑滚动、首屏逐行/逐字入场、插画分层拼装与连线流动、 滚动入场、条形图生长与数字滚动、终端逐行打出、卡片 3D 倾斜、按钮磁吸、导航滑块、 阅读进度条、页眉滚动收起、页脚大字标 - 主题切换用 View Transitions 圆形展开;同站跳转加跨页面转场 - GSAP 3.15.1 与 Lenis 1.3.27 放在 docs/js/vendor 本地加载,由 sync_docs_chrome.py 写入各页 - 无脚本、脚本加载失败、减少动态效果时都退回静态页面,内容不会被藏住 - check_design_layer.py 放宽为:@keyframes 内与白名单里的装饰伪元素可以用 opacity, 静态内容仍然不允许 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdLhesahBsuQSbGntwoQFN --- docs/css/style.css | 897 ++++++++++++++++++++++------ docs/css/tokens.css | 287 +++++---- docs/index.html | 15 +- docs/js/main.js | 31 +- docs/js/motion.js | 470 +++++++++++++++ docs/js/theme-init.js | 24 +- docs/js/vendor/LICENSE-lenis.txt | 9 + docs/js/vendor/README.md | 15 + docs/js/vendor/ScrollTrigger.min.js | 11 + docs/js/vendor/SplitText.min.js | 11 + docs/js/vendor/gsap.min.js | 11 + docs/js/vendor/lenis.min.js | 5 + docs/ota/index.html | 13 +- docs/ota/releases.html | 13 +- docs/pyspos.html | 13 +- docs/requirements.html | 13 +- docs/spaceos6.html | 13 +- docs/spaceos7.html | 13 +- docs/spaceos8-download.html | 13 +- docs/spaceos8.html | 13 +- docs/spaceos9-download.html | 13 +- docs/spaceos9.html | 13 +- tools/checks/check_design_layer.py | 30 +- tools/sync_docs_chrome.py | 13 + 24 files changed, 1596 insertions(+), 363 deletions(-) create mode 100644 docs/js/motion.js create mode 100644 docs/js/vendor/LICENSE-lenis.txt create mode 100644 docs/js/vendor/README.md create mode 100644 docs/js/vendor/ScrollTrigger.min.js create mode 100644 docs/js/vendor/SplitText.min.js create mode 100644 docs/js/vendor/gsap.min.js create mode 100644 docs/js/vendor/lenis.min.js diff --git a/docs/css/style.css b/docs/css/style.css index 4c7062f..b2c0e90 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1,8 +1,11 @@ /* ============================================================ GoutouStdio 站点样式(与 tokens.css 配合使用) - 版式:顶部导航 + 大标题首屏 + 分区正文 + 站点地图页脚。 - 没有动画和装饰图形,层次靠字号、留白和扁平色块。 + 风格:深色科技风。近黑底、玻璃质感卡片、紫→青渐变点缀、 + 首屏极光与网格背景、鼠标跟随高光。 + 动画由 js/motion.js(GSAP + ScrollTrigger + Lenis)驱动; + 这里只放静态外观、悬停过渡、@keyframes 和无脚本时的兜底。 组件只引用 --pg-*,不写死颜色与 4px 以上的尺寸(CI 会检查)。 + opacity 只出现在 @keyframes 和白名单里的装饰伪元素上(CI 会检查)。 ============================================================ */ /* ---------- 基础 ---------- */ @@ -15,8 +18,18 @@ html { scroll-padding-top: calc(var(--pg-header-h) + var(--pg-space-4)); } html[data-theme="dark"] { color-scheme: dark; } +@media (prefers-reduced-motion: no-preference) { + html { scroll-behavior: smooth; } +} + +/* Lenis 接管滚动时要求原生平滑滚动关闭 */ +html.lenis, html.lenis body { height: auto; scroll-behavior: auto; } +.lenis.lenis-stopped { overflow: clip; } +.lenis.lenis-smooth iframe { pointer-events: none; } +.lenis [data-lenis-prevent] { overscroll-behavior: contain; } body { + position: relative; margin: 0; font-family: var(--pg-font-sans); font-size: var(--pg-text-md); @@ -24,22 +37,39 @@ body { color: var(--pg-fg-1); background: var(--pg-bg-page); -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "cv11", "ss01"; + text-rendering: optimizeLegibility; +} + +/* 全站颗粒质感:纯装饰的噪点层,不含文字,不接收指针事件 */ +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 100; + pointer-events: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + opacity: var(--pg-noise-strength); } img { display: block; max-width: 100%; height: auto; } a { color: var(--pg-accent); - text-underline-offset: 0.2em; + text-decoration-thickness: var(--pg-hairline); + text-underline-offset: 0.22em; + transition: color var(--pg-dur-1) ease, text-decoration-color var(--pg-dur-1) ease; } a:hover { color: var(--pg-accent-hover); } :focus-visible { - outline: 2px solid var(--pg-focus); - outline-offset: 2px; + outline: var(--pg-stroke) solid var(--pg-focus); + outline-offset: 0.1875rem; + border-radius: var(--pg-radius-sm); } -::selection { background: var(--pg-accent-soft); } +::selection { color: var(--pg-on-ink); background: var(--pg-brand); } [hidden] { display: none !important; } @@ -59,11 +89,11 @@ a:hover { color: var(--pg-accent-hover); } position: absolute; left: var(--pg-space-3); top: -4rem; - z-index: 60; - padding: var(--pg-space-2) var(--pg-space-3); + z-index: 120; + padding: var(--pg-space-2) var(--pg-space-4); background: var(--pg-ink); color: var(--pg-on-ink); - border-radius: var(--pg-radius); + border-radius: var(--pg-radius-pill); } .skip-link:focus { top: var(--pg-space-2); color: var(--pg-on-ink); } @@ -76,45 +106,83 @@ a:hover { color: var(--pg-accent-hover); } .nowrap { white-space: nowrap; } +/* 滚动条:细、低调,跟随主题 */ +html { scrollbar-color: var(--pg-line-strong) var(--pg-bg-page); scrollbar-width: thin; } + /* ---------- 标题 ---------- */ h1, h2, h3, h4 { margin: 0; - font-family: var(--pg-font-serif); - font-weight: 400; + font-family: var(--pg-font-display); + font-weight: 650; line-height: var(--pg-leading-display); + letter-spacing: var(--pg-tracking-display); color: var(--pg-fg-1); text-wrap: balance; } -h1 { font-size: var(--pg-text-hero); letter-spacing: -0.01em; } +h1 { font-size: var(--pg-text-hero); font-weight: 700; } h2 { font-size: var(--pg-text-2xl); } -h3 { font-size: var(--pg-text-xl); line-height: var(--pg-leading-tight); } +h3 { font-size: var(--pg-text-xl); line-height: var(--pg-leading-tight); letter-spacing: -0.02em; } h4 { font-family: var(--pg-font-sans); font-size: var(--pg-text-md); font-weight: 700; line-height: var(--pg-leading-tight); + letter-spacing: 0; } p, ul, ol, dl, pre, table, figure { margin: 0; } ul, ol { padding-left: var(--pg-space-5); } li + li { margin-top: var(--pg-space-1); } +li::marker { color: var(--pg-fg-3); } strong { font-weight: 700; } .lede { max-width: var(--pg-measure); font-size: var(--pg-text-lg); + line-height: 1.7; color: var(--pg-fg-2); + text-wrap: pretty; } .small { font-size: var(--pg-text-sm); color: var(--pg-fg-3); } +/* 渐变文字:首屏标题用冷灰渐变,.grad 用品牌渐变。 + 脚本拆字后,渐变改由每一行 / 每个字自己承担,避免父元素的裁切留下静止的重影。 */ +.hero h1, +.hero h1.is-split .pg-line, +.hero h1.is-split .pg-char { + background: var(--pg-grad-text); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; +} +.hero h1.is-split { background: none; } +.hero h1 .grad, +.hero h1.is-split .grad { + background: var(--pg-grad-brand); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; +} +.pg-line-mask { padding-bottom: 0.08em; margin-bottom: -0.08em; } + /* ---------- 页眉 ---------- */ .site-header { position: sticky; top: 0; z-index: 50; - background: var(--pg-bg-page); - border-bottom: 1px solid var(--pg-line-1); -} + background: var(--pg-bg-glass); + border-bottom: var(--pg-hairline) solid var(--pg-line-glass); + -webkit-backdrop-filter: saturate(1.6) blur(var(--pg-blur)); + backdrop-filter: saturate(1.6) blur(var(--pg-blur)); + view-transition-name: site-header; + transition: transform var(--pg-dur-3) var(--pg-ease-out), + box-shadow var(--pg-dur-2) ease, background-color var(--pg-dur-2) ease; +} +.site-header.is-scrolled { box-shadow: var(--pg-shadow); } +.site-header.is-hidden { transform: translateY(-100%); } + .header-inner { display: flex; align-items: center; @@ -122,26 +190,31 @@ strong { font-weight: 700; } min-height: var(--pg-header-h); } .brand { - font-family: var(--pg-font-serif); - font-size: var(--pg-text-xl); + display: inline-flex; + align-items: center; + gap: var(--pg-space-2); + font-family: var(--pg-font-display); + font-size: var(--pg-text-lg); + font-weight: 700; + letter-spacing: -0.02em; line-height: 1; color: var(--pg-fg-1); text-decoration: none; white-space: nowrap; } -.brand { - display: inline-flex; - align-items: center; - gap: var(--pg-space-2); -} .brand:hover { color: var(--pg-fg-1); } -.brand-mark { width: 1.75rem; height: 1.75rem; flex: none; } +.brand-mark { width: 1.75rem; height: 1.75rem; flex: none; overflow: visible; } +.brand-mark rect { transform-box: fill-box; transform-origin: left center; transition: transform var(--pg-dur-3) var(--pg-ease-spring); } .brand-mark .m1 { fill: var(--pg-fg-1); } .brand-mark .m2 { fill: var(--pg-brand); } -.brand-mark .m3 { fill: var(--pg-line-strong); } +.brand-mark .m3 { fill: var(--pg-brand-2); } +.brand:hover .m1 { transform: scaleX(0.72); } +.brand:hover .m2 { transform: scaleX(0.86); transition-delay: 40ms; } +.brand:hover .m3 { transform: scaleX(0.6); transition-delay: 80ms; } -.site-nav { margin-left: auto; } +.site-nav { position: relative; margin-left: auto; } .site-nav > ul { + position: relative; display: flex; flex-wrap: wrap; align-items: center; @@ -153,24 +226,48 @@ strong { font-weight: 700; } .site-nav li + li { margin-top: 0; } .site-nav a, .site-nav summary { + position: relative; + z-index: 1; display: block; padding: var(--pg-space-2) var(--pg-space-3); font-size: var(--pg-text-sm); + font-weight: 500; color: var(--pg-fg-2); text-decoration: none; - border-radius: var(--pg-radius); + border-radius: var(--pg-radius-pill); cursor: pointer; + transition: color var(--pg-dur-1) ease, background-color var(--pg-dur-1) ease; } .site-nav a:hover, -.site-nav summary:hover { color: var(--pg-fg-1); background: var(--pg-bg-sunken); } +.site-nav summary:hover { color: var(--pg-fg-1); background: var(--pg-accent-soft); } +.site-nav.has-indicator > ul > li > a:hover, +.site-nav.has-indicator > ul > li > details > summary:hover { background: transparent; } .site-nav a[aria-current="page"], -.site-nav .menu.is-current > summary { - color: var(--pg-fg-1); - font-weight: 600; - text-decoration: underline; - text-decoration-color: var(--pg-brand); - text-decoration-thickness: 2px; - text-underline-offset: 0.4em; +.site-nav .menu.is-current > summary { color: var(--pg-fg-1); font-weight: 650; } +.site-nav > ul > li > a[aria-current="page"]::after, +.site-nav .menu.is-current > summary::after { + content: ""; + position: absolute; + left: 50%; + bottom: 0.125rem; + width: 1rem; + height: var(--pg-stroke); + margin-left: -0.5rem; + background: var(--pg-grad-brand); + border-radius: var(--pg-radius-pill); +} +/* 跟随鼠标滑动的导航高亮块,由脚本插入 */ +.nav-indicator { + position: absolute; + top: 0; + left: 0; + z-index: 0; + height: 0; + pointer-events: none; + background: var(--pg-accent-soft); + border: var(--pg-hairline) solid var(--pg-line-glass); + border-radius: var(--pg-radius-pill); + visibility: hidden; } /* 下拉菜单:原生 details,没有脚本也能点开 */ @@ -178,25 +275,31 @@ strong { font-weight: 700; } .menu > summary { list-style: none; } .menu > summary::-webkit-details-marker { display: none; } .menu > summary { display: flex; align-items: center; gap: var(--pg-space-1); } -.menu > summary .chev { width: 1em; height: 1em; vertical-align: 0; } +.menu > summary .chev { width: 1em; height: 1em; vertical-align: 0; transition: transform var(--pg-dur-2) var(--pg-ease-spring); } .menu[open] > summary .chev { transform: rotate(180deg); } -.menu > ul a { display: flex; align-items: center; gap: var(--pg-space-3); } -.menu > ul a .icon { color: var(--pg-fg-3); vertical-align: 0; } -.site-nav a .ext { width: 0.9em; height: 0.9em; margin-left: var(--pg-space-1); vertical-align: -0.1em; color: var(--pg-fg-3); } +.menu > ul a { display: flex; align-items: center; gap: var(--pg-space-3); border-radius: var(--pg-radius); } +.menu > ul a .icon { color: var(--pg-fg-3); vertical-align: 0; transition: color var(--pg-dur-1) ease, transform var(--pg-dur-2) var(--pg-ease-spring); } +.menu > ul a:hover .icon { color: var(--pg-accent); transform: scale(1.12); } +.site-nav a .ext { width: 0.9em; height: 0.9em; margin-left: var(--pg-space-1); vertical-align: -0.1em; color: var(--pg-fg-3); transition: transform var(--pg-dur-2) var(--pg-ease-out); } +.site-nav a:hover .ext { transform: translate(0.125rem, -0.125rem); } .menu > ul { position: absolute; - top: calc(100% + var(--pg-space-1)); - left: 0; + top: calc(100% + var(--pg-space-2)); + left: 50%; z-index: 55; - min-width: 13rem; + min-width: 14rem; margin: 0; padding: var(--pg-space-2); list-style: none; background: var(--pg-bg-raised); - border: 1px solid var(--pg-line-2); - border-radius: var(--pg-radius); - box-shadow: var(--pg-shadow); + border: var(--pg-hairline) solid var(--pg-line-2); + border-radius: var(--pg-radius-lg); + box-shadow: var(--pg-shadow-lg); + transform: translateX(-50%); + transform-origin: top center; } +.menu[open] > ul { animation: pg-pop var(--pg-dur-2) var(--pg-ease-out) both; } +.menu > ul li + li { margin-top: 0.125rem; } .menu > ul a { white-space: nowrap; } .header-actions { display: flex; align-items: center; gap: var(--pg-space-2); } @@ -204,63 +307,169 @@ strong { font-weight: 700; } display: inline-flex; align-items: center; justify-content: center; + width: 2.25rem; min-height: 2.25rem; - padding: 0 var(--pg-space-3); + padding: 0; font: inherit; font-size: var(--pg-text-sm); color: var(--pg-fg-2); - background: transparent; - border: 1px solid var(--pg-line-strong); - border-radius: var(--pg-radius); + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-strong); + border-radius: var(--pg-radius-pill); cursor: pointer; + transition: color var(--pg-dur-1) ease, border-color var(--pg-dur-1) ease, + transform var(--pg-dur-2) var(--pg-ease-spring), box-shadow var(--pg-dur-2) ease; } -.icon-btn:hover { color: var(--pg-fg-1); background: var(--pg-bg-sunken); } +.icon-btn:hover { color: var(--pg-fg-1); border-color: var(--pg-accent); box-shadow: var(--pg-glow); } +.icon-btn:active { transform: scale(0.92); } .nav-toggle { display: none; } -.icon-btn { width: 2.25rem; padding: 0; } .icon-btn .icon { vertical-align: 0; } .icon-sun { display: none; } body.theme-dark .icon-sun { display: inline; } body.theme-dark .icon-moon { display: none; } +.icon-sun, .icon-moon { animation: pg-spin-in var(--pg-dur-3) var(--pg-ease-spring) both; } .nav-toggle .icon-close { display: none; } .nav-toggle[aria-expanded="true"] .icon-menu { display: none; } .nav-toggle[aria-expanded="true"] .icon-close { display: inline; } +/* 阅读进度条,由脚本插入 */ +.scroll-progress { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 70; + height: var(--pg-stroke); + background: var(--pg-grad-brand); + transform: scaleX(0); + transform-origin: left center; + pointer-events: none; +} + /* ---------- 首屏 ---------- */ -.hero { padding-block: var(--pg-space-9) var(--pg-space-8); } +.hero { + position: relative; + isolation: isolate; + /* 极光比首屏宽,只在水平方向裁掉;竖直方向放开,让它延伸到半透明页眉底下 */ + overflow-x: clip; + padding-block: var(--pg-space-9) var(--pg-space-8); +} +/* 极光:三团径向渐变缓慢旋转;--ax/--ay 由脚本按鼠标位置写入做视差 */ +.hero::before { + content: ""; + position: absolute; + z-index: -2; + top: calc(var(--pg-header-h) * -1 - 10rem); + left: -15%; + right: -15%; + bottom: -6rem; + pointer-events: none; + background: + radial-gradient(38% 46% at 22% 32%, var(--pg-aurora-1), transparent 70%), + radial-gradient(32% 42% at 78% 22%, var(--pg-aurora-2), transparent 70%), + radial-gradient(28% 36% at 58% 74%, var(--pg-aurora-3), transparent 70%); + translate: calc(var(--ax, 0) * 2.5rem) calc(var(--ay, 0) * 2rem); + animation: pg-aurora var(--pg-dur-aurora) ease-in-out infinite alternate; +} +/* 网格:从顶部向下淡出 */ +.hero::after { + content: ""; + position: absolute; + z-index: -1; + top: calc(var(--pg-header-h) * -1); + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + background-image: + linear-gradient(var(--pg-grid) 1px, transparent 1px), + linear-gradient(90deg, var(--pg-grid) 1px, transparent 1px); + background-size: 3.5rem 3.5rem; + background-position: center top; + -webkit-mask-image: radial-gradient(ellipse 75% 70% at 50% 0%, black 20%, transparent 75%); + mask-image: radial-gradient(ellipse 75% 70% at 50% 0%, black 20%, transparent 75%); +} .hero .lede { margin-top: var(--pg-space-5); } .hero .actions { margin-top: var(--pg-space-6); } .hero-split { display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); + grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); gap: var(--pg-space-7); align-items: center; } -.hero-compact { padding-block: var(--pg-space-8) var(--pg-space-6); } -.hero-compact h1 { font-size: clamp(2.25rem, 4.2vw, 3rem); } -.hero-badge { margin-bottom: var(--pg-space-3); } -.hero-art { min-width: 0; } +.hero-compact { padding-block: var(--pg-space-8) var(--pg-space-7); } +.hero-compact h1, .hero-split h1 { font-size: var(--pg-text-page); } +.hero-split:has(.hero-art-svg) h1 { font-size: var(--pg-text-hero); } +.hero-badge { margin-bottom: var(--pg-space-4); } +.hero-art { position: relative; min-width: 0; } +/* 首屏截图背后的光晕 */ +.hero-art::before, +.hero-art-svg::before { + content: ""; + position: absolute; + z-index: -1; + inset: 10% 5%; + pointer-events: none; + background: radial-gradient(closest-side, var(--pg-glow-clay), transparent); +} + +/* 未开始动画前先藏住首屏,脚本接管后由 GSAP 控制;脚本 3 秒内没跑起来会自动撤掉这层 */ +html.motion:not(.motion-ready) .hero :is(h1, .lede, .actions, .hero-badge, .spec, figure) { visibility: hidden; } /* 版本、架构、许可等关键事实,一行排开 */ .spec { display: grid; - grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--pg-space-4) var(--pg-space-5); - margin-top: var(--pg-space-6); + margin-top: var(--pg-space-7); padding-top: var(--pg-space-5); - border-top: 1px solid var(--pg-line-2); + border-top: var(--pg-hairline) solid var(--pg-line-2); } -.spec dt { font-size: var(--pg-text-xs); color: var(--pg-fg-3); } -.spec dd { margin: 0; font-size: var(--pg-text-sm); overflow-wrap: anywhere; } +.spec dt { + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); + letter-spacing: var(--pg-tracking-label); + text-transform: uppercase; + color: var(--pg-fg-3); +} +.spec dd { margin: var(--pg-space-1) 0 0; font-size: var(--pg-text-sm); font-weight: 600; overflow-wrap: anywhere; } /* ---------- 分区 ---------- */ .section { - padding-block: var(--pg-space-8); - border-top: 1px solid var(--pg-line-1); + position: relative; + padding-block: var(--pg-space-9); + /* 入场前的位移和 3D 翻转可能超出屏幕宽度,横向裁掉,免得窄屏出现横向滚动 */ + overflow-x: clip; +} +/* 分区顶部一条中间亮、两端淡出的渐变细线 */ +.section::before { + content: ""; + position: absolute; + top: 0; + left: 50%; + width: min(var(--pg-container), 100%); + height: 1px; + background: var(--pg-grad-line); + transform: translateX(-50%); + pointer-events: none; } .section-tint { background: var(--pg-bg-sunken); } -.section-head { max-width: var(--pg-measure); margin-bottom: var(--pg-space-6); } -.section-head .lede { margin-top: var(--pg-space-3); } -.section-head h2 + .lede { margin-top: var(--pg-space-3); } +.section-head { max-width: var(--pg-measure); margin-bottom: var(--pg-space-7); } +.section-head h2 + .lede { margin-top: var(--pg-space-4); } + +/* 小节标题上方一小段品牌渐变短线 */ +.section-head h2::before, +.split > h2::before, +.archive-title::before { + content: ""; + display: block; + width: 2.25rem; + height: 0.1875rem; + margin-bottom: var(--pg-space-4); + background: var(--pg-grad-brand); + border-radius: var(--pg-radius-pill); +} +.archive-title::before { display: inline-block; width: 1.25rem; margin: 0 var(--pg-space-2) 0 0; vertical-align: middle; } /* 左边小节标题,右边正文 */ .split { @@ -269,60 +478,141 @@ body.theme-dark .icon-moon { display: none; } gap: var(--pg-space-7); align-items: start; } -.split > h2 { font-size: var(--pg-text-xl); line-height: var(--pg-leading-tight); } +.split > h2 { + position: sticky; + top: calc(var(--pg-header-h) + var(--pg-space-5)); + font-size: var(--pg-text-xl); + line-height: var(--pg-leading-tight); + letter-spacing: -0.02em; +} .split > div { min-width: 0; } .stack > * + * { margin-top: var(--pg-space-4); } -.stack > h3 { margin-top: var(--pg-space-6); } +.stack > h3 { margin-top: var(--pg-space-7); } .stack > h3:first-child { margin-top: 0; } .prose { max-width: var(--pg-measure); } -/* ---------- 项目色块 ---------- */ +/* ---------- 通用卡片外观 ---------- */ +.tile, +.flow > li, +.release, +.table-wrap, +.notice { + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-glass); + box-shadow: inset 0 1px 0 var(--pg-line-hi); +} + +/* 鼠标跟随高光:脚本给卡片加 .spot 并写入 --mx/--my。 + ::before 是内部柔光,::after 是只照亮描边的光圈,两者都是纯装饰。 */ +.spot { position: relative; isolation: isolate; } +.spot::before, +.spot::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + opacity: 0; + transition: opacity var(--pg-dur-2) ease; +} +.spot::before { + z-index: -1; + background: radial-gradient(22rem circle at var(--mx, 50%) var(--my, 50%), var(--pg-spot), transparent 70%); +} +.spot::after { + z-index: 1; + padding: var(--pg-hairline); + background: radial-gradient(16rem circle at var(--mx, 50%) var(--my, 50%), var(--pg-brand), transparent 70%); + -webkit-mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(black 0 0) content-box exclude, linear-gradient(black 0 0); +} +.spot:hover::before, +.spot:hover::after { opacity: 1; } + +/* ---------- 项目卡片 ---------- */ .tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: var(--pg-space-4); + gap: var(--pg-space-5); + perspective: 75rem; } .tile { + --glow: var(--pg-glow-clay); display: flex; flex-direction: column; gap: var(--pg-space-3); min-width: 0; padding: var(--pg-space-6); - background: var(--pg-bg-raised); - border-radius: var(--pg-radius-lg); + overflow: hidden; + border-radius: var(--pg-radius-xl); + transform-style: preserve-3d; + transition: border-color var(--pg-dur-2) ease, box-shadow var(--pg-dur-2) ease; } +.tile:hover { border-color: var(--pg-line-strong); box-shadow: var(--pg-shadow-lg); } .tile h3 { font-size: var(--pg-text-2xl); } .tile p { color: var(--pg-fg-2); } -.tile .status { margin: 0; font-size: var(--pg-text-sm); color: var(--pg-fg-2); } +.tile .status { display: flex; flex-wrap: wrap; align-items: center; gap: var(--pg-space-2); margin: 0; font-size: var(--pg-text-sm); color: var(--pg-fg-2); } .tile-links { display: flex; flex-wrap: wrap; - gap: var(--pg-space-1) var(--pg-space-4); + gap: var(--pg-space-1) var(--pg-space-5); margin-top: auto; - padding-top: var(--pg-space-3); + padding-top: var(--pg-space-4); + font-size: var(--pg-text-sm); font-weight: 600; + border-top: var(--pg-hairline) solid var(--pg-line-glass); } -.tile a { color: var(--pg-fg-1); } +.tile a { color: var(--pg-fg-1); text-decoration: none; } .tile a:hover { color: var(--pg-accent-hover); } -.tile-clay { background: var(--pg-tile-clay); } -.tile-sky { background: var(--pg-tile-sky); } -.tile-sage { background: var(--pg-tile-sage); } +.tile-links a { display: inline-flex; align-items: center; gap: var(--pg-space-1); } +.tile-links a .icon { transition: transform var(--pg-dur-2) var(--pg-ease-spring); } +.tile-links a:hover .icon { transform: translateY(0.1875rem); } +.tile-clay { --glow: var(--pg-glow-clay); background: radial-gradient(120% 70% at 100% 0%, var(--glow), transparent 55%), var(--pg-tile-clay); } +.tile-sky { --glow: var(--pg-glow-sky); background: radial-gradient(120% 70% at 100% 0%, var(--glow), transparent 55%), var(--pg-tile-sky); } +.tile-sage { --glow: var(--pg-glow-sage); background: radial-gradient(120% 70% at 100% 0%, var(--glow), transparent 55%), var(--pg-tile-sage); } -.archive-title { margin: var(--pg-space-8) 0 var(--pg-space-3); } +.archive-title { margin: var(--pg-space-8) 0 var(--pg-space-4); font-size: var(--pg-text-xl); } +.archive-title .icon { color: var(--pg-fg-3); } /* ---------- 行列表:历史、存档、版本 ---------- */ .rows { margin: 0; padding: 0; list-style: none; } .rows > li { + position: relative; display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: var(--pg-space-4); margin: 0; - padding: var(--pg-space-4) 0; - border-top: 1px solid var(--pg-line-2); + padding: var(--pg-space-5) 0; + border-top: var(--pg-hairline) solid var(--pg-line-2); + transition: background-color var(--pg-dur-2) ease; +} +.rows > li:last-child { border-bottom: var(--pg-hairline) solid var(--pg-line-2); } +.rows > li::before { + content: ""; + position: absolute; + top: -1px; + left: 0; + width: 100%; + height: 1px; + background: var(--pg-grad-brand); + transform: scaleX(0); + transform-origin: left center; + transition: transform var(--pg-dur-3) var(--pg-ease-out); +} +.rows > li:hover::before { transform: scaleX(1); } +.rows .key { + font-family: var(--pg-font-display); + font-size: var(--pg-text-lg); + font-weight: 650; + letter-spacing: -0.01em; + line-height: var(--pg-leading-tight); + transition: color var(--pg-dur-2) ease; } -.rows > li:last-child { border-bottom: 1px solid var(--pg-line-2); } -.rows .key { font-family: var(--pg-font-serif); font-size: var(--pg-text-lg); line-height: var(--pg-leading-tight); } +.rows > li:hover .key { color: var(--pg-accent); } +.rows .key a { color: inherit; } +.rows p { color: var(--pg-fg-2); } .rows-wide > li { grid-template-columns: 12rem minmax(0, 1fr); } .rows p + p { margin-top: var(--pg-space-2); } @@ -332,58 +622,89 @@ code, kbd, pre { font-size: 0.875em; } code { - padding: 0.1em 0.35em; - background: var(--pg-bg-sunken); - border: 1px solid var(--pg-line-1); - border-radius: var(--pg-radius); + padding: 0.1em 0.4em; + background: var(--pg-accent-soft); + border: var(--pg-hairline) solid var(--pg-line-glass); + border-radius: var(--pg-radius-sm); overflow-wrap: anywhere; } -.section-tint code { background: var(--pg-bg-raised); } pre { position: relative; - padding: var(--pg-space-4) var(--pg-space-5); - line-height: 1.65; - background: var(--pg-bg-sunken); - border: 1px solid var(--pg-line-2); - border-radius: var(--pg-radius); + padding: var(--pg-space-5); + line-height: 1.7; + color: var(--pg-term-fg); + background: var(--pg-term-bg); + border: var(--pg-hairline) solid var(--pg-line-glass); + border-radius: var(--pg-radius-lg); + box-shadow: inset 0 1px 0 var(--pg-line-hi), var(--pg-shadow); overflow-x: auto; } -.section-tint pre { background: var(--pg-bg-raised); } pre code { padding: 0; + color: inherit; background: none; border: 0; font-size: inherit; overflow-wrap: normal; } +/* 终端:顶部窗口栏和三个圆点 */ pre.terminal { - color: var(--pg-term-fg); - background: var(--pg-term-bg); - border-color: var(--pg-term-bg); + padding-top: calc(var(--pg-space-7) + var(--pg-space-1)); border-radius: var(--pg-radius-lg); } +pre.terminal::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2.25rem; + background: + radial-gradient(circle at 1.125rem 50%, var(--pg-term-red) 0.3125rem, transparent 0.34rem), + radial-gradient(circle at 2.125rem 50%, var(--pg-term-yellow) 0.3125rem, transparent 0.34rem), + radial-gradient(circle at 3.125rem 50%, var(--pg-term-green) 0.3125rem, transparent 0.34rem), + var(--pg-term-bar); + border-bottom: var(--pg-hairline) solid var(--pg-line-glass); +} +pre.terminal > code { display: block; } pre.terminal .dim { color: var(--pg-term-dim); } pre.terminal .prompt { color: var(--pg-term-accent); user-select: none; } +.term-caret { + display: inline-block; + width: 0.55em; + height: 1.1em; + margin-left: 0.15em; + vertical-align: -0.2em; + background: var(--pg-term-accent); + animation: pg-blink 1.1s steps(1) infinite; +} .copy-btn { position: absolute; - top: var(--pg-space-2); - right: var(--pg-space-2); - padding: var(--pg-space-1) var(--pg-space-2); + top: var(--pg-space-3); + right: var(--pg-space-3); + padding: var(--pg-space-1) var(--pg-space-3); font: inherit; font-family: var(--pg-font-sans); font-size: var(--pg-text-xs); line-height: 1.4; - color: var(--pg-fg-2); - background: var(--pg-bg-raised); - border: 1px solid var(--pg-line-strong); - border-radius: var(--pg-radius); + color: var(--pg-term-fg); + background: var(--pg-term-bar); + border: var(--pg-hairline) solid var(--pg-line-strong); + border-radius: var(--pg-radius-pill); cursor: pointer; + transition: border-color var(--pg-dur-1) ease, transform var(--pg-dur-2) var(--pg-ease-spring); } -.copy-btn:hover { color: var(--pg-fg-1); } -pre:has(.copy-btn) { padding-right: var(--pg-space-8); } +.copy-btn:hover { border-color: var(--pg-term-accent); } +.copy-btn:active { transform: scale(0.94); } +.copy-btn.is-done { color: var(--pg-term-accent); border-color: var(--pg-term-accent); animation: pg-bump var(--pg-dur-3) var(--pg-ease-spring); } +pre:has(.copy-btn) { padding-right: var(--pg-space-9); } /* ---------- 表格 ---------- */ -.table-wrap { overflow-x: auto; } +.table-wrap { + overflow-x: auto; + padding: var(--pg-space-1) var(--pg-space-5); + border-radius: var(--pg-radius-lg); +} table { width: 100%; border-collapse: collapse; @@ -393,57 +714,71 @@ th, td { padding: var(--pg-space-3) var(--pg-space-4) var(--pg-space-3) 0; text-align: left; vertical-align: top; - border-bottom: 1px solid var(--pg-line-2); + border-bottom: var(--pg-hairline) solid var(--pg-line-2); } +tbody tr:last-child > * { border-bottom: 0; } +tbody tr { transition: background-color var(--pg-dur-1) ease; } +tbody tr:hover { background: var(--pg-accent-soft); } thead th { white-space: nowrap; - font-size: var(--pg-text-xs); + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); font-weight: 600; + letter-spacing: var(--pg-tracking-label); + text-transform: uppercase; color: var(--pg-fg-3); - border-bottom: 1px solid var(--pg-line-strong); + border-bottom: var(--pg-hairline) solid var(--pg-line-strong); } -tbody th { font-weight: 600; white-space: nowrap; } -td.num { text-align: right; font-family: var(--pg-font-mono); white-space: nowrap; } +tbody th { font-weight: 650; white-space: nowrap; } +td { color: var(--pg-fg-2); } +td.num { text-align: right; font-family: var(--pg-font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--pg-fg-1); } /* ---------- 提示块与徽标 ---------- */ -.notice { - padding: var(--pg-space-4) var(--pg-space-5); - background: var(--pg-bg-sunken); - border: 1px solid var(--pg-line-2); - border-radius: var(--pg-radius); -} .notice { display: flex; align-items: flex-start; gap: var(--pg-space-3); + padding: var(--pg-space-4) var(--pg-space-5); + border-radius: var(--pg-radius-lg); } .notice > div { min-width: 0; } .notice > div > * + * { margin-top: var(--pg-space-2); } .notice-icon { margin-top: 0.2em; color: var(--pg-accent); } .notice-warn .notice-icon { color: var(--pg-warn-fg); } .notice-title { font-weight: 700; } -.notice-info { background: var(--pg-accent-soft); border-color: var(--pg-accent-soft); } +.notice-info { background: var(--pg-accent-soft); border-color: var(--pg-line-glass); } .notice-warn { background: var(--pg-warn-bg); border-color: var(--pg-warn-line); } -.section-tint .notice { background: var(--pg-bg-raised); } -.section-tint .notice-warn { background: var(--pg-warn-bg); } .badge { - display: inline-block; - padding: 0 var(--pg-space-2); + display: inline-flex; + align-items: center; + gap: var(--pg-space-1); + padding: 0 var(--pg-space-3); font-family: var(--pg-font-sans); font-size: var(--pg-text-xs); font-weight: 600; - line-height: 1.7; + line-height: 1.75; white-space: nowrap; color: var(--pg-mute-fg); background: var(--pg-mute-bg); - border: 1px solid var(--pg-mute-line); + border: var(--pg-hairline) solid var(--pg-mute-line); border-radius: var(--pg-radius-pill); vertical-align: middle; } .badge-ok { color: var(--pg-ok-fg); background: var(--pg-ok-bg); border-color: var(--pg-ok-line); } .badge-warn { color: var(--pg-warn-fg); background: var(--pg-warn-bg); border-color: var(--pg-warn-line); } .badge-bad { color: var(--pg-bad-fg); background: var(--pg-bad-bg); border-color: var(--pg-bad-line); } +/* 状态徽标前的呼吸圆点(只给没有图标的徽标) */ +.badge:is(.badge-ok, .badge-warn):not(:has(.icon))::before { + content: ""; + flex: none; + width: 0.375rem; + height: 0.375rem; + background: currentColor; + border-radius: 50%; + box-shadow: 0 0 0 0 currentColor; + animation: pg-ping 2.4s var(--pg-ease-out) infinite; +} /* ---------- 按钮 ---------- */ .actions { @@ -452,32 +787,58 @@ td.num { text-align: right; font-family: var(--pg-font-mono); white-space: nowra gap: var(--pg-space-3); } .button { - display: inline-block; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + overflow: hidden; padding: var(--pg-space-3) var(--pg-space-5); font-size: var(--pg-text-sm); - font-weight: 600; + font-weight: 650; line-height: var(--pg-leading-tight); text-decoration: none; color: var(--pg-on-ink); background: var(--pg-ink); - border: 1px solid var(--pg-ink); - border-radius: var(--pg-radius); + border: var(--pg-hairline) solid var(--pg-ink); + border-radius: var(--pg-radius-pill); + box-shadow: var(--pg-shadow); + transition: background-color var(--pg-dur-1) ease, border-color var(--pg-dur-2) ease, + box-shadow var(--pg-dur-2) ease; } -.button:hover { color: var(--pg-on-ink); background: var(--pg-ink-hover); border-color: var(--pg-ink-hover); } +/* 悬停时一道斜光扫过 */ +.button::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(105deg, transparent 30%, var(--pg-shine) 50%, transparent 70%); + transform: translateX(-110%); + transition: transform var(--pg-dur-3) var(--pg-ease-out); +} +.button:hover::after { transform: translateX(110%); } +.button:hover { color: var(--pg-on-ink); background: var(--pg-ink-hover); border-color: var(--pg-ink-hover); box-shadow: var(--pg-glow); } +.button:active { box-shadow: var(--pg-shadow); } .button-secondary { color: var(--pg-fg-1); - background: transparent; + background: var(--pg-bg-card); border-color: var(--pg-line-strong); + box-shadow: none; + -webkit-backdrop-filter: blur(var(--pg-blur)); + backdrop-filter: blur(var(--pg-blur)); } -.button-secondary:hover { color: var(--pg-fg-1); background: var(--pg-bg-sunken); border-color: var(--pg-line-strong); } +.button-secondary:hover { color: var(--pg-fg-1); background: var(--pg-accent-soft); border-color: var(--pg-accent); } +.button .icon { margin-right: var(--pg-space-2); vertical-align: -0.2em; transition: transform var(--pg-dur-2) var(--pg-ease-spring); } +.button:hover .icon { transform: translateY(0.125rem); } /* ---------- 图片 ---------- */ figure img { - border: 1px solid var(--pg-line-2); + border: var(--pg-hairline) solid var(--pg-line-glass); border-radius: var(--pg-radius-lg); + box-shadow: var(--pg-shadow-lg); } +.img-frame { overflow: hidden; border-radius: var(--pg-radius-lg); } figcaption { - margin-top: var(--pg-space-2); + margin-top: var(--pg-space-3); font-size: var(--pg-text-sm); color: var(--pg-fg-3); } @@ -488,18 +849,20 @@ figcaption { flex-wrap: wrap; align-items: center; gap: var(--pg-space-2) var(--pg-space-4); - margin-bottom: var(--pg-space-4); + margin-bottom: var(--pg-space-5); } .release-tools button[aria-pressed] { padding: var(--pg-space-1) var(--pg-space-4); font: inherit; font-size: var(--pg-text-sm); color: var(--pg-fg-2); - background: transparent; - border: 1px solid var(--pg-line-strong); + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-strong); border-radius: var(--pg-radius-pill); cursor: pointer; + transition: color var(--pg-dur-1) ease, background-color var(--pg-dur-2) ease, border-color var(--pg-dur-2) ease; } +.release-tools button[aria-pressed]:hover { color: var(--pg-fg-1); border-color: var(--pg-accent); } .release-tools button[aria-pressed="true"] { color: var(--pg-on-ink); background: var(--pg-ink); @@ -507,33 +870,46 @@ figcaption { } .release-tools input { min-width: 14rem; - padding: var(--pg-space-1) var(--pg-space-3); + padding: var(--pg-space-2) var(--pg-space-4); font: inherit; font-size: var(--pg-text-sm); color: var(--pg-fg-1); - background: var(--pg-bg-raised); - border: 1px solid var(--pg-line-strong); - border-radius: var(--pg-radius); + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-strong); + border-radius: var(--pg-radius-pill); + transition: border-color var(--pg-dur-1) ease, box-shadow var(--pg-dur-2) ease; } -.release-count { margin-bottom: var(--pg-space-3); font-size: var(--pg-text-sm); color: var(--pg-fg-3); } +.release-tools input:focus { outline: none; border-color: var(--pg-accent); box-shadow: var(--pg-glow); } +.release-count { margin-bottom: var(--pg-space-4); font-size: var(--pg-text-sm); color: var(--pg-fg-3); } .release { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: var(--pg-space-5); - padding: var(--pg-space-5) 0; - border-top: 1px solid var(--pg-line-2); + padding: var(--pg-space-6); + border-radius: var(--pg-radius-xl); } +.release + .release { margin-top: var(--pg-space-4); } .release h3 { font-size: var(--pg-text-xl); } -.release-meta { margin-top: var(--pg-space-1); font-size: var(--pg-text-sm); color: var(--pg-fg-3); } +.release-meta { margin-top: var(--pg-space-1); font-family: var(--pg-font-mono); font-size: var(--pg-text-xs); color: var(--pg-fg-3); } .release > div > * + * { margin-top: var(--pg-space-3); } /* ---------- 页脚 ---------- */ .site-footer { + position: relative; + overflow: hidden; padding-block: var(--pg-space-8) var(--pg-space-5); background: var(--pg-bg-sunken); - border-top: 1px solid var(--pg-line-1); font-size: var(--pg-text-sm); } +.site-footer::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: var(--pg-grad-line); +} .footer-grid { display: grid; grid-template-columns: 1.6fr repeat(4, minmax(0, 1fr)); @@ -542,15 +918,23 @@ figcaption { .footer-about p { margin-top: var(--pg-space-3); max-width: 22rem; color: var(--pg-fg-3); } .footer-title { margin: 0 0 var(--pg-space-3); - font-family: var(--pg-font-sans); - font-size: var(--pg-text-sm); - font-weight: 700; + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); + font-weight: 600; + letter-spacing: var(--pg-tracking-label); + text-transform: uppercase; line-height: var(--pg-leading-tight); + color: var(--pg-fg-3); } .site-footer ul { margin: 0; padding: 0; list-style: none; } .site-footer li + li { margin-top: var(--pg-space-2); } .site-footer a { color: var(--pg-fg-2); text-decoration: none; } -.site-footer a:hover { color: var(--pg-accent-hover); text-decoration: underline; } +.site-footer a:hover { color: var(--pg-fg-1); } +.site-footer nav a { + background: linear-gradient(currentColor, currentColor) left bottom / 0 1px no-repeat; + transition: color var(--pg-dur-1) ease, background-size var(--pg-dur-2) var(--pg-ease-out); +} +.site-footer nav a:hover { background-size: 100% 1px; } .footer-base { display: flex; flex-wrap: wrap; @@ -558,12 +942,31 @@ figcaption { gap: var(--pg-space-2) var(--pg-space-5); margin-top: var(--pg-space-7); padding-top: var(--pg-space-4); - border-top: 1px solid var(--pg-line-2); + border-top: var(--pg-hairline) solid var(--pg-line-2); font-size: var(--pg-text-xs); color: var(--pg-fg-3); } .footer-base p { margin: 0; } - +/* 页脚底部的超大字标,由脚本插入,aria-hidden,纯装饰 */ +.footer-mark { + margin-top: var(--pg-space-6); + font-family: var(--pg-font-display); + font-size: clamp(3.5rem, 15vw, 13rem); + font-weight: 800; + letter-spacing: -0.05em; + line-height: 0.8; + text-align: center; + white-space: nowrap; + user-select: none; + pointer-events: none; + background: var(--pg-grad-brand); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; + -webkit-mask-image: linear-gradient(180deg, black 10%, transparent 95%); + mask-image: linear-gradient(180deg, black 10%, transparent 95%); +} /* ---------- 图标 ---------- */ .sprite { position: absolute; width: 0; height: 0; overflow: hidden; } @@ -579,6 +982,7 @@ figcaption { vertical-align: -0.25em; } .icon-box { + position: relative; display: inline-grid; place-items: center; flex: none; @@ -586,33 +990,51 @@ figcaption { height: 3rem; color: var(--pg-fg-1); background: var(--pg-bg-raised); + border: var(--pg-hairline) solid var(--pg-line-glass); border-radius: var(--pg-radius); + box-shadow: inset 0 1px 0 var(--pg-line-hi), var(--pg-shadow); } -.icon-box .icon { width: 1.625rem; height: 1.625rem; vertical-align: 0; } -.section-tint .icon-box { background: var(--pg-bg-page); } -.button .icon { margin-right: var(--pg-space-2); vertical-align: -0.2em; } -.badge .icon { width: 1em; height: 1em; margin-right: var(--pg-space-1); vertical-align: -0.15em; } -.tile-icon { margin-bottom: var(--pg-space-2); } +/* 图标框外圈一层渐变描边 */ +.icon-box::before { + content: ""; + position: absolute; + inset: calc(var(--pg-hairline) * -1); + padding: var(--pg-hairline); + border-radius: inherit; + background: linear-gradient(140deg, var(--pg-brand), transparent 45%, transparent 60%, var(--pg-brand-2)); + -webkit-mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(black 0 0) content-box exclude, linear-gradient(black 0 0); + pointer-events: none; +} +.icon-box .icon { width: 1.5rem; height: 1.5rem; vertical-align: 0; transition: transform var(--pg-dur-3) var(--pg-ease-spring); } +.tile:hover .icon-box .icon, +.points > li:hover .icon-box .icon, +.flow > li:hover .icon-box .icon { transform: scale(1.14) rotate(-6deg); } +.button .icon { margin-right: var(--pg-space-2); } +.badge .icon { width: 1em; height: 1em; vertical-align: -0.15em; } +.tile-icon { margin-bottom: var(--pg-space-3); } .split-head { display: flex; flex-direction: column; gap: var(--pg-space-3); align-items: flex-start; } .icon-line { display: flex; align-items: baseline; gap: var(--pg-space-3); } -.icon-line .icon { position: relative; top: 0.25em; color: var(--pg-fg-3); } +.icon-line .icon { position: relative; top: 0.25em; color: var(--pg-accent); } /* ---------- 要点条:每页顶部的四件事 ---------- */ .points { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--pg-space-5); + gap: var(--pg-space-6); margin: 0; padding: 0; list-style: none; } .points > li { display: flex; flex-direction: column; gap: var(--pg-space-2); margin: 0; } -.points .icon-box { margin-bottom: var(--pg-space-1); background: var(--pg-bg-sunken); } -.points + .notice { margin-top: var(--pg-space-6); } +.points .icon-box { margin-bottom: var(--pg-space-2); color: var(--pg-accent); } +.points + .notice { margin-top: var(--pg-space-7); } .points strong { - font-family: var(--pg-font-serif); + font-family: var(--pg-font-display); font-size: var(--pg-text-lg); - font-weight: 400; + font-weight: 650; + letter-spacing: -0.01em; line-height: var(--pg-leading-tight); } .points p { font-size: var(--pg-text-sm); color: var(--pg-fg-2); } @@ -636,12 +1058,12 @@ figcaption { min-width: 0; margin: 0; padding: var(--pg-space-5); - background: var(--pg-bg-raised); - border: 1px solid var(--pg-line-2); border-radius: var(--pg-radius-lg); + transition: border-color var(--pg-dur-2) ease, translate var(--pg-dur-3) var(--pg-ease-out); } -.section-tint .flow > li { background: var(--pg-bg-page); } -.flow > li + li::before { +.flow > li:hover { border-color: var(--pg-line-strong); translate: 0 -0.25rem; } +/* 卡片之间的箭头:挂在 .flow-top 上,好让卡片本身的 ::before/::after 留给鼠标高光 */ +.flow > li + li > .flow-top::before { content: ""; position: absolute; top: 50%; @@ -649,16 +1071,24 @@ figcaption { width: 0.6rem; height: 0.6rem; margin-top: -0.3rem; - border-top: 2px solid var(--pg-line-strong); - border-right: 2px solid var(--pg-line-strong); + border-top: var(--pg-stroke) solid var(--pg-brand); + border-right: var(--pg-stroke) solid var(--pg-brand); transform: rotate(45deg); } .flow-top { display: flex; align-items: center; justify-content: space-between; } -.flow-top .icon-box { background: var(--pg-bg-sunken); } -.section-tint .flow-top .icon-box { background: var(--pg-bg-sunken); } -.flow-n { font-family: var(--pg-font-serif); font-size: var(--pg-text-xl); color: var(--pg-brand); } +.flow-n { + font-family: var(--pg-font-mono); + font-size: var(--pg-text-xl); + font-weight: 700; + background: var(--pg-grad-brand); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; +} .flow-title { font-weight: 700; line-height: var(--pg-leading-tight); } .flow p { font-size: var(--pg-text-sm); color: var(--pg-fg-2); } +.flow p.flow-title { color: var(--pg-fg-1); } .flow code { font-size: var(--pg-text-xs); } .flow > li > a { margin-top: auto; padding-top: var(--pg-space-2); font-size: var(--pg-text-sm); font-weight: 600; } .flow > li > code { margin-top: auto; } @@ -672,27 +1102,42 @@ figcaption { list-style: none; } .layers > li { + position: relative; display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--pg-space-1) var(--pg-space-4); margin: 0; - padding: var(--pg-space-3) var(--pg-space-4); - background: var(--pg-bg-sunken); + padding: var(--pg-space-3) var(--pg-space-5); + overflow: hidden; + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-glass); border-radius: var(--pg-radius); + transition: translate var(--pg-dur-3) var(--pg-ease-out), border-color var(--pg-dur-2) ease; +} +.layers > li::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 0.1875rem; + background: var(--layer, var(--pg-line-strong)); } +.layers > li:hover { translate: 0.375rem 0; border-color: var(--pg-line-strong); } .layers strong { font-weight: 700; } .layers span { font-size: var(--pg-text-sm); color: var(--pg-fg-2); } -.layers .layer-user { background: var(--pg-tile-sky); } -.layers .layer-compat { background: var(--pg-tile-sage); } -.layers .layer-lib { background: var(--pg-tile-sage); } -.layers .layer-kernel { background: var(--pg-tile-clay); } +.layers .layer-user { --layer: var(--pg-ref-cyan); background: var(--pg-tile-sky); } +.layers .layer-compat { --layer: var(--pg-ref-emerald); background: var(--pg-tile-sage); } +.layers .layer-lib { --layer: var(--pg-ref-emerald); background: var(--pg-tile-sage); } +.layers .layer-kernel { --layer: var(--pg-ref-violet); background: var(--pg-tile-clay); } +.layers .layer-boot { --layer: var(--pg-ref-pink); } .layers .layer-abi { background: transparent; - border: 2px dashed var(--pg-line-strong); + border: var(--pg-stroke) dashed var(--pg-line-strong); } -.section-tint .layers .layer-boot { background: var(--pg-bg-raised); } +.layers .layer-abi::before { display: none; } /* ---------- 条形图:表格里的数量对比 ---------- */ .bar { @@ -701,7 +1146,7 @@ figcaption { height: 0.375rem; margin: 0 0 var(--pg-space-1); overflow: hidden; - background: var(--pg-line-1); + background: var(--pg-line-2); border-radius: var(--pg-radius-pill); } .num .bar { margin-left: auto; } @@ -709,26 +1154,74 @@ figcaption { display: block; width: var(--w); height: 100%; - background: var(--pg-brand); + background: var(--pg-grad-brand); + border-radius: inherit; + transform-origin: left center; } /* ---------- 插画 ---------- */ -.art { display: block; width: 100%; max-width: 30rem; height: auto; margin-inline: auto; } +.art { display: block; width: 100%; max-width: 30rem; height: auto; margin-inline: auto; overflow: visible; } .art text { font-family: var(--pg-font-sans); fill: var(--pg-fg-1); } .art .art-sub { fill: var(--pg-fg-2); } .art .art-note { fill: var(--pg-fg-3); } +.art rect { stroke: var(--pg-line-glass); stroke-width: 1; } .art-l1 { fill: var(--pg-tile-sky); } .art-l2 { fill: var(--pg-tile-clay); } .art-l3 { fill: var(--pg-tile-sage); } +.art rect.art-l2 { stroke: var(--pg-brand); stroke-width: 1.5; } .art-slot { fill: var(--pg-bg-raised); stroke: var(--pg-line-strong); stroke-width: 2; } -.art-slot-on { fill: var(--pg-tile-clay); stroke: var(--pg-brand); stroke-width: 2; } +.art rect.art-slot { stroke: var(--pg-line-strong); stroke-width: 2; } +.art rect.art-slot-on { fill: var(--pg-tile-clay); stroke: var(--pg-brand); stroke-width: 2; } .art-line { fill: none; stroke: var(--pg-fg-3); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } -.art-brand { fill: none; stroke: var(--pg-brand); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; } -.art-ico { fill: none; stroke: var(--pg-fg-2); stroke-width: 1.1; stroke-linecap: round; stroke-linejoin: round; } +.art-brand { fill: none; stroke: var(--pg-brand-2); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; } +.art-ico { fill: none; stroke: var(--pg-accent); stroke-width: 1.1; stroke-linecap: round; stroke-linejoin: round; } .stack .art { max-width: 34rem; margin-inline: 0; } -.hero-art-svg { min-width: 0; } +.hero-art-svg { position: relative; min-width: 0; } .hero-art-svg figcaption { max-width: 30rem; margin-inline: auto; } +/* ---------- 关键帧 ---------- */ +@keyframes pg-aurora { + 0% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(6deg) scale(1.08); } + 100% { transform: rotate(-4deg) scale(1.02); } +} +@keyframes pg-pop { + from { opacity: 0; transform: translateX(-50%) translateY(-0.375rem) scale(0.97); } + to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); } +} +@keyframes pg-spin-in { + from { opacity: 0; transform: rotate(-90deg) scale(0.5); } + to { opacity: 1; transform: rotate(0deg) scale(1); } +} +@keyframes pg-ping { + 0% { box-shadow: 0 0 0 0 currentColor; } + 70%, 100% { box-shadow: 0 0 0 0.375rem transparent; } +} +@keyframes pg-blink { + 0%, 50% { opacity: 1; } + 50.01%, 100% { opacity: 0; } +} +@keyframes pg-bump { + 0% { transform: scale(1); } + 40% { transform: scale(1.12); } + 100% { transform: scale(1); } +} +@keyframes pg-vt-out { + to { opacity: 0; transform: translateY(-0.75rem) scale(0.985); } +} +@keyframes pg-vt-in { + from { opacity: 0; transform: translateY(1.25rem) scale(1.01); } +} + +/* ---------- 跨页面转场(Chrome / Edge / Safari 18.2+;其他浏览器照常跳转) ---------- */ +@view-transition { navigation: auto; } +::view-transition-old(root) { animation: var(--pg-dur-2) var(--pg-ease-in-out) both pg-vt-out; } +::view-transition-new(root) { animation: var(--pg-dur-3) var(--pg-ease-out) both pg-vt-in; } +/* 切换主题时由脚本用圆形展开动画,替换掉默认的淡入淡出 */ +html.vt-theme .site-header { view-transition-name: none; } +html.vt-theme::view-transition-old(root), +html.vt-theme::view-transition-new(root) { animation: none; mix-blend-mode: normal; } + /* ---------- 窄屏 ---------- */ @media (max-width: 62rem) { .tiles { grid-template-columns: minmax(0, 1fr); } @@ -740,19 +1233,21 @@ figcaption { @media (max-width: 56rem) { .container { padding-inline: var(--pg-space-4); } - .hero { padding-block: var(--pg-space-7) var(--pg-space-6); } + .hero { padding-block: var(--pg-space-8) var(--pg-space-7); } .hero-split, .split, .release { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-5); } - .section { padding-block: var(--pg-space-7); } + .split > h2 { position: static; } + .section { padding-block: var(--pg-space-8); } .rows > li, .rows-wide > li { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-1); } .points { grid-template-columns: minmax(0, 1fr); } .flow, .flow-4, .flow-5 { --cols: 1; gap: var(--pg-space-6); } - .flow > li + li::before { + .flow > li + li > .flow-top::before { top: -1.05rem; left: 50%; margin: 0 0 0 -0.3rem; transform: rotate(135deg); } .hero-art-svg { max-width: 24rem; } + .release { padding: var(--pg-space-5); } /* 没有脚本时,导航直接在页眉里折行,仍然可用。 */ .header-inner { flex-wrap: wrap; padding-block: var(--pg-space-2); } @@ -771,10 +1266,26 @@ figcaption { border: 0; background: transparent; padding-block: 0; + transform: none; + animation: none; + } + .site-nav > ul > li > a[aria-current="page"]::after, + .site-nav .menu.is-current > summary::after { left: var(--pg-space-3); margin-left: 0; } +} + +/* 用户要求减少动态效果时,关掉所有装饰性动画与过渡 */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } + .hero::before { animation: none; translate: none; } + @view-transition { navigation: none; } } @media print { - .site-header, .copy-btn, .skip-link, .site-footer { display: none; } + .site-header, .copy-btn, .skip-link, .site-footer, .scroll-progress { display: none; } + body::after, .hero::before, .hero::after { display: none; } .section { padding-block: var(--pg-space-5); } } diff --git a/docs/css/tokens.css b/docs/css/tokens.css index e4ffa0b..606f74d 100644 --- a/docs/css/tokens.css +++ b/docs/css/tokens.css @@ -3,47 +3,59 @@ 只放值与语义映射,不放组件样式。组件只准引用 --pg-*。 三层:原始值(:root)→ 语义(按用途命名)→ 主题覆写。 尺寸一律用 rem,便于跟随用户的字号设置。 - 配色:暖米色底、近黑暖灰字、陶土色点缀。 + 配色:深色科技风。近黑蓝底、冷灰字、紫→青渐变做品牌色; + 默认深色,浅色作为可切换的备选主题。 ============================================================ */ :root { /* ---- 原始色板(主题无关) ---- */ - --pg-ref-ivory: #faf9f5; - --pg-ref-ivory-dark: #f0eee6; + --pg-ref-void: #05060a; + --pg-ref-night: #0a0c13; + --pg-ref-snow: #f6f7fb; --pg-ref-white: #ffffff; - --pg-ref-slate: #141413; - --pg-ref-clay: #d97757; - --pg-ref-clay-deep: #a64b2a; - --pg-ref-clay-darker: #84391f; + --pg-ref-violet: #7c6cff; + --pg-ref-violet-deep: #4b3fd6; + --pg-ref-cyan: #22d3ee; + --pg-ref-cyan-deep: #0b6f86; + --pg-ref-emerald: #34d399; + --pg-ref-pink: #f472b6; /* 终端面板:两个主题下固定同一套深色 */ - --pg-term-bg: #1f1e1d; - --pg-term-fg: #f0eee6; - --pg-term-dim: #b0aea5; - --pg-term-accent: #eba589; - - /* ---- 字体:标题用衬线,正文用无衬线,代码用等宽 ---- - 衬线栈里 Georgia 管拉丁字母;中文在 macOS 落到宋体,其余系统落到雅黑。 */ - --pg-font-serif: Georgia, "Source Serif 4", "Songti SC", "Noto Serif CJK SC", - "Source Han Serif SC", "Microsoft YaHei", "PingFang SC", serif; - --pg-font-sans: system-ui, -apple-system, "Segoe UI", "PingFang SC", + --pg-term-bg: #07080d; + --pg-term-fg: #e8eaf2; + --pg-term-dim: #9198ab; + --pg-term-accent: #67e3f6; + --pg-term-bar: #12141d; + --pg-term-red: #ff5f57; + --pg-term-yellow: #febc2e; + --pg-term-green: #28c840; + + /* ---- 字体:标题与正文都用无衬线,代码与标签用等宽 ---- */ + --pg-font-display: "Inter Display", Inter, system-ui, -apple-system, "Segoe UI", + "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", + "Noto Sans SC", sans-serif; + --pg-font-sans: Inter, system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", sans-serif; - --pg-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, + --pg-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; /* ---- 字阶与行高 ---- */ + --pg-text-2xs: 0.75rem; --pg-text-xs: 0.8125rem; --pg-text-sm: 0.9375rem; --pg-text-md: 1rem; --pg-text-lg: 1.1875rem; --pg-text-xl: 1.5rem; - --pg-text-2xl: 2.125rem; - --pg-text-hero: clamp(2.5rem, 5.4vw, 4rem); + --pg-text-2xl: clamp(1.875rem, 3.2vw, 2.625rem); + --pg-text-hero: clamp(2.75rem, 6.6vw, 5.5rem); + --pg-text-page: clamp(2.5rem, 5vw, 4rem); - --pg-leading-body: 1.8; + --pg-leading-body: 1.75; --pg-leading-tight: 1.3; - --pg-leading-display: 1.18; + --pg-leading-display: 1.08; + --pg-tracking-display: -0.035em; + --pg-tracking-label: 0.14em; /* ---- 间距与尺寸 ---- */ --pg-space-1: 0.25rem; @@ -53,110 +65,171 @@ --pg-space-5: 1.5rem; --pg-space-6: 2rem; --pg-space-7: 3rem; - --pg-space-8: 4.5rem; - --pg-space-9: 6rem; + --pg-space-8: 5rem; + --pg-space-9: 7.5rem; - --pg-radius: 0.5rem; - --pg-radius-lg: 1rem; + --pg-radius-sm: 0.375rem; + --pg-radius: 0.625rem; + --pg-radius-lg: 1.125rem; + --pg-radius-xl: 1.5rem; --pg-radius-pill: 6rem; --pg-header-h: 4.25rem; - --pg-container: 72rem; - --pg-measure: 42rem; + --pg-container: 75rem; + --pg-measure: 44rem; + --pg-hairline: 0.0625rem; + --pg-stroke: 0.125rem; + --pg-blur: 1rem; + --pg-blur-lg: 1.5rem; + + /* ---- 动效:时长与缓动 ---- */ + --pg-dur-1: 160ms; + --pg-dur-2: 320ms; + --pg-dur-3: 640ms; + --pg-dur-aurora: 28s; + --pg-ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --pg-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + /* 弹簧:刚度 180、阻尼 14 预先算好的曲线;不支持 linear() 的浏览器整条声明失效,回到 ease */ + --pg-ease-spring: linear(0, 0.009, 0.035 2.1%, 0.141 4.4%, 0.723 12.9%, 0.938 16.7%, + 1.017, 1.077, 1.121, 1.149 24.3%, 1.159, 1.163 27%, 1.154, 1.129 32.8%, + 1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.975 57.1%, 0.997 69.8%, 1.003 76.9%, 1); } -/* ---- 语义层:亮色(默认) ---- */ +/* ---- 语义层:亮色(备选) ---- */ :root, body:not(.theme-dark) { color-scheme: light; - --pg-bg-page: var(--pg-ref-ivory); + --pg-bg-page: var(--pg-ref-snow); --pg-bg-raised: var(--pg-ref-white); - --pg-bg-sunken: var(--pg-ref-ivory-dark); + --pg-bg-sunken: #edeff5; + --pg-bg-glass: rgb(255 255 255 / 0.72); + --pg-bg-card: rgb(255 255 255 / 0.8); - --pg-fg-1: var(--pg-ref-slate); - --pg-fg-2: #3d3d3a; - --pg-fg-3: #5e5d59; + --pg-fg-1: #0b0d14; + --pg-fg-2: #3b4152; + --pg-fg-3: #575e72; - --pg-line-1: #e8e6dc; - --pg-line-2: #d6d3c5; + --pg-line-1: #e4e7ef; + --pg-line-2: #d5d9e4; /* 控件边界:按钮、输入框的描边,3.0+ 对比度。 line-1/line-2 只做装饰性分隔线,不承载信息。 */ - --pg-line-strong: #8a877d; - - --pg-accent: var(--pg-ref-clay-deep); - --pg-accent-hover: var(--pg-ref-clay-darker); - --pg-accent-soft: #f6e3d8; - --pg-brand: var(--pg-ref-clay); - - /* 主按钮:深色底、米色字 */ - --pg-ink: var(--pg-ref-slate); - --pg-ink-hover: #3d3d3a; - --pg-on-ink: var(--pg-ref-ivory); - - /* 首页项目色块:三种扁平底色 */ - --pg-tile-clay: #f4e1d6; - --pg-tile-sky: #dfe8f1; - --pg-tile-sage: #e5e9d9; - - --pg-ok-fg: #065f46; - --pg-ok-bg: #e9f5ef; - --pg-ok-line: #9fd2bb; - --pg-warn-fg: #8a3d0c; - --pg-warn-bg: #fbf0dc; - --pg-warn-line: #e0be85; + --pg-line-strong: #838a9e; + --pg-line-glass: rgb(11 13 20 / 0.08); + --pg-line-hi: rgb(255 255 255 / 0.9); + + --pg-accent: var(--pg-ref-violet-deep); + --pg-accent-hover: #3a2fb8; + --pg-accent-soft: #ecebff; + --pg-brand: var(--pg-ref-violet); + --pg-brand-2: var(--pg-ref-cyan-deep); + --pg-grad-brand: linear-gradient(120deg, var(--pg-ref-violet-deep) 0%, var(--pg-ref-violet) 45%, var(--pg-ref-cyan-deep) 100%); + --pg-grad-text: linear-gradient(180deg, #0b0d14 30%, #4a5066 100%); + --pg-grad-line: linear-gradient(90deg, transparent, rgb(75 63 214 / 0.5), rgb(11 111 134 / 0.5), transparent); + + /* 主按钮:深色底、白字 */ + --pg-ink: #0b0d14; + --pg-ink-hover: #262a38; + --pg-on-ink: var(--pg-ref-white); + + /* 首页项目卡片:三种带色调的底 */ + --pg-tile-clay: #f1efff; + --pg-tile-sky: #eaf7fb; + --pg-tile-sage: #eaf7f1; + --pg-glow-clay: rgb(124 108 255 / 0.32); + --pg-glow-sky: rgb(34 211 238 / 0.32); + --pg-glow-sage: rgb(52 211 153 / 0.32); + + /* 首屏极光与网格背景(纯装饰) */ + --pg-aurora-1: rgb(124 108 255 / 0.22); + --pg-aurora-2: rgb(34 211 238 / 0.18); + --pg-aurora-3: rgb(244 114 182 / 0.12); + --pg-grid: rgb(11 13 20 / 0.06); + --pg-spot: rgb(124 108 255 / 0.12); + --pg-noise-strength: 0.03; + --pg-shine: rgb(255 255 255 / 0.28); + + --pg-ok-fg: #05603f; + --pg-ok-bg: #e6f6ef; + --pg-ok-line: #9fd8bf; + --pg-warn-fg: #844006; + --pg-warn-bg: #fdf3e1; + --pg-warn-line: #e8c58c; --pg-bad-fg: #991b1b; - --pg-bad-bg: #fbecec; - --pg-bad-line: #e6a8a8; - --pg-mute-fg: #3d3d3a; - --pg-mute-bg: var(--pg-ref-ivory-dark); - --pg-mute-line: #d6d3c5; - - --pg-focus: var(--pg-ref-clay-deep); - --pg-shadow: 0 0.5rem 1.5rem rgb(20 20 19 / 0.14); + --pg-bad-bg: #fdecec; + --pg-bad-line: #eaa9a9; + --pg-mute-fg: #3b4152; + --pg-mute-bg: #edeff5; + --pg-mute-line: #d5d9e4; + + --pg-focus: var(--pg-ref-violet-deep); + --pg-shadow: 0 0.75rem 2rem rgb(11 13 20 / 0.1), 0 0.125rem 0.375rem rgb(11 13 20 / 0.06); + --pg-shadow-lg: 0 2rem 4rem rgb(11 13 20 / 0.14); + --pg-glow: 0 0 0 0.0625rem rgb(75 63 214 / 0.18), 0 0.5rem 2rem rgb(124 108 255 / 0.22); } -/* ---- 语义层:暗色 ---- */ +/* ---- 语义层:暗色(默认) ---- */ body.theme-dark { color-scheme: dark; - --pg-bg-page: #1f1e1d; - --pg-bg-raised: #2a2927; - --pg-bg-sunken: #181716; - - --pg-fg-1: #faf9f5; - --pg-fg-2: #cfccc1; - --pg-fg-3: #a5a297; - - --pg-line-1: #34322f; - --pg-line-2: #45433f; - --pg-line-strong: #7b7970; - - --pg-accent: #e8926f; - --pg-accent-hover: #f1ae92; - --pg-accent-soft: #3a2a22; - --pg-brand: var(--pg-ref-clay); - - --pg-ink: #faf9f5; - --pg-ink-hover: #e0ddd2; - --pg-on-ink: #141413; - - --pg-tile-clay: #3a2b24; - --pg-tile-sky: #232c36; - --pg-tile-sage: #272c20; - - --pg-ok-fg: #7dd3ae; - --pg-ok-bg: #10251d; - --pg-ok-line: #1f4a3a; - --pg-warn-fg: #f0c57c; - --pg-warn-bg: #2a2113; - --pg-warn-line: #57431f; - --pg-bad-fg: #f1a3a3; - --pg-bad-bg: #2b1517; + --pg-bg-page: var(--pg-ref-void); + --pg-bg-raised: #0e1018; + --pg-bg-sunken: #090b11; + --pg-bg-glass: rgb(10 12 19 / 0.66); + --pg-bg-card: rgb(17 19 29 / 0.6); + + --pg-fg-1: #f1f3f9; + --pg-fg-2: #b6bccc; + --pg-fg-3: #8a91a5; + + --pg-line-1: #161923; + --pg-line-2: #222632; + --pg-line-strong: #5d6477; + --pg-line-glass: rgb(255 255 255 / 0.08); + --pg-line-hi: rgb(255 255 255 / 0.1); + + --pg-accent: #a99fff; + --pg-accent-hover: #cdc7ff; + --pg-accent-soft: #15142c; + --pg-brand: var(--pg-ref-violet); + --pg-brand-2: var(--pg-ref-cyan); + --pg-grad-brand: linear-gradient(120deg, var(--pg-ref-violet) 0%, #a78bfa 40%, var(--pg-ref-cyan) 100%); + --pg-grad-text: linear-gradient(180deg, #ffffff 30%, #8f96aa 100%); + --pg-grad-line: linear-gradient(90deg, transparent, rgb(124 108 255 / 0.7), rgb(34 211 238 / 0.7), transparent); + + --pg-ink: #f1f3f9; + --pg-ink-hover: #ffffff; + --pg-on-ink: #05060a; + + --pg-tile-clay: #120f24; + --pg-tile-sky: #0b1820; + --pg-tile-sage: #0b1a16; + --pg-glow-clay: rgb(124 108 255 / 0.45); + --pg-glow-sky: rgb(34 211 238 / 0.4); + --pg-glow-sage: rgb(52 211 153 / 0.4); + + --pg-aurora-1: rgb(124 108 255 / 0.3); + --pg-aurora-2: rgb(34 211 238 / 0.18); + --pg-aurora-3: rgb(244 114 182 / 0.14); + --pg-grid: rgb(255 255 255 / 0.045); + --pg-spot: rgb(124 108 255 / 0.16); + --pg-noise-strength: 0.05; + --pg-shine: rgb(124 108 255 / 0.3); + + --pg-ok-fg: #6ee7b7; + --pg-ok-bg: #0a1f18; + --pg-ok-line: #1b4a39; + --pg-warn-fg: #f6c87a; + --pg-warn-bg: #211a0c; + --pg-warn-line: #54421d; + --pg-bad-fg: #f5a3a3; + --pg-bad-bg: #261215; --pg-bad-line: #5c2629; - --pg-mute-fg: #cfccc1; - --pg-mute-bg: #2a2927; - --pg-mute-line: #45433f; - - --pg-focus: #e8926f; - --pg-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.5); + --pg-mute-fg: #c3c8d6; + --pg-mute-bg: #12141d; + --pg-mute-line: #2a2e3b; + + --pg-focus: #a99fff; + --pg-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.5), 0 0.125rem 0.375rem rgb(0 0 0 / 0.4); + --pg-shadow-lg: 0 2rem 5rem rgb(0 0 0 / 0.6); + --pg-glow: 0 0 0 0.0625rem rgb(124 108 255 / 0.35), 0 0.5rem 2.5rem rgb(124 108 255 / 0.35); } diff --git a/docs/index.html b/docs/index.html index 7ec34b3..2b32ed3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,14 +6,14 @@ GoutouStdio - - + + - +
-

几个学生,
在写操作系统。

+

几个学生,
在写操作系统。

GoutouStdio 是学生自发组织的开源小组,2022 年开始做操作系统。现在维护两个项目:用 Python 写的 PySpOS,和用 Rust 写内核的 SpaceOS 9。

了解 PySpOS @@ -239,7 +239,12 @@
- + + + + + + diff --git a/docs/js/main.js b/docs/js/main.js index 3d558fa..d9013cb 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -1,5 +1,6 @@ /* 站点交互:主题切换、导航菜单、代码块复制。 - 三项都是渐进增强,脚本失效时页面内容与导航仍然可用。 */ + 三项都是渐进增强,脚本失效时页面内容与导航仍然可用。 + 滚动与入场动画在 motion.js 里,和这里互不依赖。 */ (function () { "use strict"; @@ -11,9 +12,10 @@ function saveTheme(theme) { try { localStorage.setItem(KEY, theme); } catch (e) { /* 隐私模式下忽略 */ } } - function preferredTheme() { - if (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) return "light"; - return "dark"; + // 站点默认暗色,访客手动切到浅色才用浅色(与 theme-init.js 一致)。 + function preferredTheme() { return "dark"; } + function reducedMotion() { + return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; } function initTheme() { @@ -31,8 +33,24 @@ } apply(readTheme() || preferredTheme(), false); if (!btn) return; + // 支持 View Transitions 的浏览器里,新主题从按钮位置以圆形展开;其余浏览器直接切换。 btn.addEventListener("click", function () { - apply(document.body.classList.contains("theme-dark") ? "light" : "dark", true); + var next = document.body.classList.contains("theme-dark") ? "light" : "dark"; + if (!document.startViewTransition || reducedMotion()) { apply(next, true); return; } + var rect = btn.getBoundingClientRect(); + var x = rect.left + rect.width / 2; + var y = rect.top + rect.height / 2; + var r = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y)); + var root = document.documentElement; + root.classList.add("vt-theme"); + var vt = document.startViewTransition(function () { apply(next, true); }); + vt.ready.then(function () { + root.animate( + { clipPath: ["circle(0 at " + x + "px " + y + "px)", "circle(" + r + "px at " + x + "px " + y + "px)"] }, + { duration: 560, easing: "cubic-bezier(0.65, 0, 0.35, 1)", pseudoElement: "::view-transition-new(root)" } + ); + }).catch(function () { /* 被打断时主题已经切好,忽略 */ }); + vt.finished.then(function () { root.classList.remove("vt-theme"); }, function () { root.classList.remove("vt-theme"); }); }); window.addEventListener("storage", function (e) { if (e.key === KEY) apply(e.newValue || preferredTheme(), false); @@ -102,8 +120,9 @@ var text = copy.textContent.replace(/\n$/, ""); function done(ok) { btn.textContent = ok ? "已复制" : "复制失败"; + btn.classList.toggle("is-done", ok); status.textContent = ok ? "已复制到剪贴板" : "复制失败,请手动选择文本"; - window.setTimeout(function () { btn.textContent = "复制"; }, 1600); + window.setTimeout(function () { btn.textContent = "复制"; btn.classList.remove("is-done"); }, 1600); } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); }); diff --git a/docs/js/motion.js b/docs/js/motion.js new file mode 100644 index 0000000..6961ee5 --- /dev/null +++ b/docs/js/motion.js @@ -0,0 +1,470 @@ +/* 站点动画:GSAP + ScrollTrigger + SplitText + Lenis(都在 js/vendor,本地加载)。 + 全部是渐进增强: + - 访客要求减少动态效果,或者任何一个库没加载上,就什么都不做,页面保持静态完整; + - 没有脚本时 theme-init.js 不会打上 motion 标记,内容不会被藏起来; + - 动画只改 transform / opacity / clip-path,不改文字内容(数字滚动结束后与原文一致)。 */ +(function () { + "use strict"; + + var root = document.documentElement; + var gsap = window.gsap; + var ScrollTrigger = window.ScrollTrigger; + var SplitText = window.SplitText; + var mm = window.matchMedia ? function (q) { return window.matchMedia(q).matches; } : function () { return false; }; + + if (!gsap || !ScrollTrigger || mm("(prefers-reduced-motion: reduce)")) { + root.classList.remove("motion"); + return; + } + gsap.registerPlugin(ScrollTrigger); + if (SplitText) gsap.registerPlugin(SplitText); + + var finePointer = mm("(hover: hover) and (pointer: fine)"); + var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); }; + var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); }; + + gsap.defaults({ ease: "power3.out", duration: 0.9 }); + + /* ---------- 平滑滚动 ---------- */ + var lenis = null; + function initLenis() { + if (!window.Lenis) return; + // 锚点跳转交给 Lenis,它会读取 html 的 scroll-padding-top,避开吸顶页眉。 + lenis = new window.Lenis({ lerp: 0.1, wheelMultiplier: 1, anchors: true }); + lenis.on("scroll", ScrollTrigger.update); + gsap.ticker.add(function (time) { lenis.raf(time * 1000); }); + gsap.ticker.lagSmoothing(0); + } + + /* ---------- 阅读进度条与页眉 ---------- */ + function initChrome() { + var bar = document.createElement("div"); + bar.className = "scroll-progress"; + bar.setAttribute("aria-hidden", "true"); + document.body.appendChild(bar); + gsap.to(bar, { scaleX: 1, ease: "none", scrollTrigger: { start: 0, end: "max", scrub: 0.3 } }); + + var header = $(".site-header"); + var nav = $("#site-nav"); + if (!header) return; + // 往下滚时收起页眉,往上滚立刻出现;菜单展开或焦点在页眉里时不收。 + ScrollTrigger.create({ + start: 0, + end: "max", + onUpdate: function (self) { + var y = self.scroll(); + header.classList.toggle("is-scrolled", y > 8); + var busy = (nav && nav.classList.contains("open")) || header.querySelector("details[open]") || + header.contains(document.activeElement); + header.classList.toggle("is-hidden", self.direction === 1 && y > 320 && !busy); + } + }); + } + + /* ---------- 导航:跟随鼠标的高亮块、窄屏菜单展开 ---------- */ + function initNav() { + var nav = $("#site-nav"); + if (!nav) return; + var list = $(":scope > ul", nav); + var items = $$(":scope > ul > li > a, :scope > ul > li > details > summary", nav); + + var media = gsap.matchMedia(); + media.add("(min-width: 56.01rem) and (hover: hover)", function () { + var pill = document.createElement("span"); + pill.className = "nav-indicator"; + pill.setAttribute("aria-hidden", "true"); + list.prepend(pill); + nav.classList.add("has-indicator"); + var shown = false; + function moveTo(el) { + var vars = { x: el.offsetLeft, y: el.offsetTop, width: el.offsetWidth, height: el.offsetHeight }; + if (!shown) { gsap.set(pill, vars); shown = true; } + gsap.to(pill, Object.assign({ autoAlpha: 1, duration: 0.45, ease: "expo.out", overwrite: true }, vars)); + } + function hide() { shown = false; gsap.to(pill, { autoAlpha: 0, duration: 0.25, overwrite: true }); } + items.forEach(function (el) { + el.addEventListener("mouseenter", function () { moveTo(el); }); + el.addEventListener("focus", function () { moveTo(el); }); + }); + list.addEventListener("mouseleave", hide); + return function () { pill.remove(); nav.classList.remove("has-indicator"); }; + }); + + // 窄屏:菜单打开时逐项落下 + new MutationObserver(function () { + if (nav.classList.contains("open")) { + gsap.from($$(":scope > ul > li", nav), { y: -10, autoAlpha: 0, duration: 0.45, stagger: 0.04, ease: "power2.out" }); + } + }).observe(nav, { attributes: true, attributeFilter: ["class"] }); + } + + /* ---------- 文字拆分 ---------- */ + // 带
的标题按行包一层遮罩,逐行从下方升起;行内的 .grad 渐变不受影响。 + function splitLines(el) { + var groups = [[]]; + Array.prototype.slice.call(el.childNodes).forEach(function (n) { + if (n.nodeName === "BR") groups.push([]); + else groups[groups.length - 1].push(n); + }); + el.textContent = ""; + return groups.map(function (nodes) { + var mask = document.createElement("span"); + var line = document.createElement("span"); + mask.className = "pg-line-mask"; + line.className = "pg-line"; + mask.style.display = "block"; + mask.style.overflow = "clip"; + line.style.display = "block"; + nodes.forEach(function (n) { line.appendChild(n); }); + mask.appendChild(line); + el.appendChild(mask); + return line; + }); + } + + function splitChars(el) { + if (!SplitText) return null; + var split = SplitText.create(el, { type: "words,chars", wordsClass: "pg-word", charsClass: "pg-char" }); + return split.chars; + } + + /* ---------- 插画:分层拼装、连线描边、虚线流动 ---------- */ + function prepStroke(path) { + var len = 0; + try { len = path.getTotalLength(); } catch (e) { len = 0; } + if (!len) return 0; + gsap.set(path, { strokeDasharray: len, strokeDashoffset: len }); + return len; + } + + function artTimeline(svg) { + var tl = gsap.timeline({ paused: true }); + var blocks = $$("rect", svg); + var texts = $$("text", svg); + var strokes = $$(".art-brand, .art-line", svg); + var icons = $$(".art-ico", svg); + tl.from(blocks, { y: -28, autoAlpha: 0, scale: 0.94, transformOrigin: "50% 50%", duration: 0.9, stagger: 0.14, ease: "back.out(1.4)" }, 0) + .from(texts, { x: -12, autoAlpha: 0, duration: 0.6, stagger: 0.04 }, 0.25) + .from(icons, { scale: 0, autoAlpha: 0, transformOrigin: "50% 50%", duration: 0.7, stagger: 0.12, ease: "back.out(2)" }, 0.45); + strokes.forEach(function (p, i) { + if (!prepStroke(p)) return; + tl.to(p, { strokeDashoffset: 0, duration: 0.8, ease: "power2.inOut" }, 0.55 + i * 0.12); + }); + // 描完之后变成流动的虚线,表示数据在层与层之间传递 + tl.call(function () { + strokes.forEach(function (p) { + gsap.set(p, { strokeDasharray: "6 7", strokeDashoffset: 0 }); + gsap.to(p, { strokeDashoffset: -26, duration: 1.4, ease: "none", repeat: -1 }); + }); + icons.forEach(function (g, i) { + gsap.to(g, { y: -3, duration: 1.8 + i * 0.3, ease: "sine.inOut", yoyo: true, repeat: -1 }); + }); + }); + return tl; + } + + /* ---------- 终端:逐行打出 ---------- */ + function typeTerminal(pre) { + var code = $("code", pre) || pre; + var lines = Math.max(1, code.textContent.replace(/\n$/, "").split("\n").length); + var caret = document.createElement("span"); + caret.className = "term-caret"; + caret.setAttribute("aria-hidden", "true"); + gsap.set(code, { display: "block", clipPath: "inset(0% 0% 100% 0%)" }); + return gsap.to(code, { + clipPath: "inset(0% 0% 0% 0%)", + duration: Math.min(2.4, lines * 0.11), + ease: "steps(" + lines + ")", + paused: true, + onComplete: function () { code.appendChild(caret); gsap.set(code, { clearProps: "clipPath" }); } + }); + } + + /* ---------- 首屏入场 ---------- */ + function initHero() { + var hero = $(".hero"); + if (!hero) return; + var tl = gsap.timeline({ defaults: { ease: "expo.out", duration: 1.1 }, delay: 0.05 }); + + var badge = $(".hero-badge", hero); + if (badge) tl.from(badge, { y: 16, autoAlpha: 0, duration: 0.8 }, 0); + + var h1 = $("h1", hero); + if (h1) { + if (h1.querySelector("br")) { + h1.classList.add("is-split"); + tl.from(splitLines(h1), { yPercent: 110, rotation: 2, duration: 1.2, stagger: 0.12 }, 0.08); + } else { + var chars = splitChars(h1); + if (chars && chars.length) { + h1.classList.add("is-split"); + tl.from(chars, { + yPercent: 90, rotationX: -80, autoAlpha: 0, transformOrigin: "50% 100% -0.5em", + duration: 1, stagger: { each: 0.03, from: "start" } + }, 0.08); + } else { + tl.from(h1, { y: 30, autoAlpha: 0 }, 0.08); + } + } + } + + var lede = $(".lede", hero); + if (lede) tl.from(lede, { y: 24, autoAlpha: 0 }, 0.4); + var acts = $$(".actions > *", hero); + if (acts.length) tl.from(acts, { y: 20, autoAlpha: 0, stagger: 0.08 }, 0.55); + var spec = $$(".spec > div", hero); + if (spec.length) tl.from(spec, { y: 16, autoAlpha: 0, stagger: 0.06 }, 0.65); + + var fig = $("figure", hero); + if (fig) { + var svg = $("svg.art", fig); + if (svg) { + var art = artTimeline(svg); + tl.from(fig, { autoAlpha: 0, duration: 0.4 }, 0.2); + tl.add(function () { art.play(); }, 0.25); + var cap = $("figcaption", fig); + if (cap) tl.from(cap, { y: 10, autoAlpha: 0, duration: 0.8 }, 1.2); + } else { + tl.from(fig, { y: 50, scale: 0.95, rotationX: 10, transformPerspective: 1200, autoAlpha: 0, duration: 1.4 }, 0.25); + var term = $("pre.terminal", fig); + if (term) { + var typing = typeTerminal(term); + tl.add(function () { typing.play(); }, 0.8); + } + // 往下滚时截图比文字移得快一点,拉出前后景深 + var img = $("img", fig); + if (img) { + gsap.to(fig, { + yPercent: -10, ease: "none", + scrollTrigger: { trigger: hero, start: "top top", end: "bottom top", scrub: true } + }); + } + } + } + + // 极光随鼠标轻微偏移 + if (finePointer) { + var ax = gsap.quickTo(hero, "--ax", { duration: 1.6, ease: "power3" }); + var ay = gsap.quickTo(hero, "--ay", { duration: 1.6, ease: "power3" }); + hero.addEventListener("pointermove", function (e) { + var r = hero.getBoundingClientRect(); + ax((e.clientX - r.left) / r.width - 0.5); + ay((e.clientY - r.top) / r.height - 0.5); + }); + hero.addEventListener("pointerleave", function () { ax(0); ay(0); }); + } + } + + /* ---------- 滚动入场 ---------- */ + function reveal(targets, from, opts) { + targets = targets.filter(Boolean); + if (!targets.length) return; + opts = opts || {}; + gsap.set(targets, Object.assign({ autoAlpha: 0 }, from)); + ScrollTrigger.batch(targets, { + start: opts.start || "top 88%", + once: true, + onEnter: function (batch) { + var to = { autoAlpha: 1, x: 0, y: 0, scale: 1, rotationX: 0, rotation: 0, stagger: opts.stagger || 0.09, + duration: opts.duration || 1, ease: opts.ease || "expo.out", overwrite: true, + clearProps: "transform,opacity,visibility" }; + gsap.to(batch, to); + if (opts.onEnter) opts.onEnter(batch); + } + }); + } + + // 去掉祖先已经在列表里的元素,避免父子叠加两次动画 + function topmost(els) { + return els.filter(function (el) { + return !els.some(function (other) { return other !== el && other.contains(el); }); + }); + } + + function popIcons(batch) { + var icons = batch.map(function (el) { return $(".icon-box", el); }).filter(Boolean); + if (icons.length) { + gsap.fromTo(icons, { scale: 0.5, rotation: -20 }, + { scale: 1, rotation: 0, duration: 0.9, stagger: 0.09, ease: "back.out(2.2)", delay: 0.15, clearProps: "transform" }); + } + } + + function initReveals() { + var main = $("main"); + if (!main) return; + var heroEl = $(".hero", main); + var notHero = function (el) { return !(heroEl && heroEl.contains(el)); }; + + // 小节标题逐字浮现 + reveal($$(".archive-title", main), { x: -20 }); + $$(".section-head h2, .split > h2", main).filter(notHero).forEach(function (h) { + var chars = splitChars(h); + if (!chars || !chars.length) { reveal([h], { y: 30 }); return; } + gsap.from(chars, { + yPercent: 80, autoAlpha: 0, rotationX: -60, transformOrigin: "50% 100%", duration: 0.9, + stagger: 0.025, ease: "expo.out", + scrollTrigger: { trigger: h, start: "top 88%", once: true } + }); + }); + reveal($$(".section-head .lede", main).filter(notHero), { y: 24 }); + + reveal($$(".tile", main), { y: 70, rotationX: -14, transformOrigin: "50% 100%" }, + { stagger: 0.12, duration: 1.2, onEnter: popIcons }); + reveal($$(".points > li", main), { y: 40 }, { stagger: 0.1, onEnter: popIcons }); + reveal($$(".flow > li", main), { y: 40, scale: 0.96 }, { stagger: 0.12, onEnter: popIcons }); + reveal($$(".rows > li", main), { x: -30 }, { stagger: 0.07 }); + reveal($$(".layers > li", main), { x: 50 }, { stagger: 0.07 }); + + var general = topmost($$([ + ".notice", ".table-wrap", "main pre", ".stack > p", ".stack > ul", ".stack > ol", ".stack > dl", + ".stack > .actions", ".stack > h3", "figure", ".prose > *", ".release-tools" + ].join(","), main).filter(notHero).filter(function (el) { + return !el.closest(".tile, .points, .flow, .rows, .layers"); + })); + reveal(general, { y: 36 }, { stagger: 0.08 }); + + // 插画:进入视口后再拼装 + $$("svg.art", main).filter(notHero).forEach(function (svg) { + var tl = artTimeline(svg); + ScrollTrigger.create({ trigger: svg, start: "top 80%", once: true, onEnter: function () { tl.play(); } }); + }); + + // 不在首屏的终端也逐行打出 + $$("pre.terminal", main).filter(notHero).forEach(function (pre) { + var t = typeTerminal(pre); + ScrollTrigger.create({ trigger: pre, start: "top 80%", once: true, onEnter: function () { t.play(); } }); + }); + + // 首屏以外的大图:遮罩展开 + 轻微缩放 + $$("figure img", main).filter(notHero).forEach(function (img) { + gsap.fromTo(img, { clipPath: "inset(8% 6% 8% 6% round 1.125rem)", scale: 1.06 }, { + clipPath: "inset(0% 0% 0% 0% round 1.125rem)", scale: 1, duration: 1.6, ease: "expo.out", + clearProps: "clipPath,transform", + scrollTrigger: { trigger: img, start: "top 85%", once: true } + }); + }); + } + + /* ---------- 数据:条形图生长、数字滚动 ---------- */ + function initData() { + $$(".table-wrap").forEach(function (wrap) { + var bars = $$(".bar > i", wrap); + var nums = $$("td.num", wrap); + if (!bars.length && !nums.length) return; + gsap.set(bars, { scaleX: 0 }); + ScrollTrigger.create({ + trigger: wrap, start: "top 82%", once: true, + onEnter: function () { + gsap.to(bars, { scaleX: 1, duration: 1.6, stagger: 0.08, ease: "expo.out", delay: 0.2 }); + nums.forEach(function (td, i) { + var node = td.lastChild; + if (!node || node.nodeType !== 3) return; + var text = node.nodeValue.trim(); + if (!/^\d{1,3}(,\d{3})*$|^\d+$/.test(text)) return; + var end = parseInt(text.replace(/,/g, ""), 10); + var grouped = text.indexOf(",") !== -1; + var obj = { v: 0 }; + gsap.to(obj, { + v: end, duration: 1.6, delay: 0.2 + i * 0.08, ease: "expo.out", + onUpdate: function () { + var n = Math.round(obj.v); + node.nodeValue = grouped ? n.toLocaleString("en-US") : String(n); + }, + onComplete: function () { node.nodeValue = text; } + }); + }); + } + }); + }); + } + + /* ---------- 卡片:鼠标跟随高光、3D 倾斜;按钮:磁吸 ---------- */ + function initPointer() { + if (!finePointer) return; + function track(el) { + el.classList.add("spot"); + el.addEventListener("pointermove", function (e) { + var r = el.getBoundingClientRect(); + el.style.setProperty("--mx", (e.clientX - r.left) + "px"); + el.style.setProperty("--my", (e.clientY - r.top) + "px"); + }); + } + $$(".tile, .flow > li, .release, .table-wrap").forEach(track); + window.pgTrackSpot = track; + + $$(".tile").forEach(function (tile) { + var rx = gsap.quickTo(tile, "rotationX", { duration: 0.6, ease: "power3" }); + var ry = gsap.quickTo(tile, "rotationY", { duration: 0.6, ease: "power3" }); + gsap.set(tile, { transformPerspective: 900 }); + tile.addEventListener("pointermove", function (e) { + var r = tile.getBoundingClientRect(); + ry(((e.clientX - r.left) / r.width - 0.5) * 8); + rx(-((e.clientY - r.top) / r.height - 0.5) * 8); + }); + tile.addEventListener("pointerleave", function () { rx(0); ry(0); }); + }); + + $$(".button").forEach(function (btn) { + var mx = gsap.quickTo(btn, "x", { duration: 0.5, ease: "power3" }); + var my = gsap.quickTo(btn, "y", { duration: 0.5, ease: "power3" }); + btn.addEventListener("pointermove", function (e) { + var r = btn.getBoundingClientRect(); + mx((e.clientX - r.left - r.width / 2) * 0.22); + my((e.clientY - r.top - r.height / 2) * 0.35); + }); + btn.addEventListener("pointerleave", function () { + gsap.to(btn, { x: 0, y: 0, duration: 0.9, ease: "elastic.out(1, 0.4)", overwrite: true }); + }); + }); + } + + /* ---------- 版本历史:脚本渲染出来的列表也有入场 ---------- */ + function initDynamicLists() { + var host = $("#version-history"); + if (!host) return; + new MutationObserver(function () { + var items = $$(":scope > .release", host).slice(0, 12); + if (!items.length) return; + if (window.pgTrackSpot) items.forEach(window.pgTrackSpot); + gsap.fromTo(items, { y: 28, autoAlpha: 0 }, { + y: 0, autoAlpha: 1, duration: 0.7, stagger: 0.05, ease: "expo.out", overwrite: true, + clearProps: "transform,opacity,visibility" + }); + }).observe(host, { childList: true }); + } + + /* ---------- 页脚:超大字标随滚动升起 ---------- */ + function initFooter() { + var footer = $(".site-footer"); + if (!footer) return; + var mark = document.createElement("div"); + mark.className = "footer-mark"; + mark.setAttribute("aria-hidden", "true"); + mark.textContent = "GoutouStdio"; + footer.appendChild(mark); + gsap.from(mark, { + yPercent: 45, autoAlpha: 0, ease: "none", + scrollTrigger: { trigger: footer, start: "top bottom", end: "bottom bottom", scrub: 0.6 } + }); + reveal($$(".footer-grid > *", footer), { y: 24 }, { stagger: 0.07, start: "top 95%" }); + } + + function init() { + initLenis(); + initChrome(); + initNav(); + initHero(); + root.classList.add("motion-ready"); + initReveals(); + initData(); + initPointer(); + initDynamicLists(); + initFooter(); + // 图片和字体到齐后重新量一次触发位置 + window.addEventListener("load", function () { ScrollTrigger.refresh(); }); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { ScrollTrigger.refresh(); }); + } + + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); + else init(); +})(); diff --git a/docs/js/theme-init.js b/docs/js/theme-init.js index d96046b..a689057 100644 --- a/docs/js/theme-init.js +++ b/docs/js/theme-init.js @@ -1,24 +1,28 @@ /* 主题预初始化:放在 的第一行,正文绘制前就定好主题,避免闪一下。 约定:body.theme-dark = 暗色,无类名 = 亮色。 - 没有保存过选择时跟随系统;系统也没给出偏好时用暗色。 - 同时给 打上 js 标记,窄屏侧栏据此才收成抽屉;没有脚本时导航照常显示。 */ + 站点默认暗色;只有访客手动切到浅色并保存过,才用浅色。 + 同时给 打上 js 标记,窄屏侧栏据此才收成抽屉;没有脚本时导航照常显示。 + 访客没有要求减少动态效果时再打上 motion 标记:首屏内容先藏起来,等 motion.js 接管后播放入场动画。 + motion.js 3 秒内没有接管(脚本加载失败、被拦截),就撤掉标记,内容照常显示。 */ (function () { var KEY = "pg-theme"; var root = document.documentElement; var saved = null; try { saved = localStorage.getItem(KEY); } catch (e) { saved = null; } - var theme; - if (saved === "light" || saved === "dark") { - theme = saved; - } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) { - theme = "light"; - } else { - theme = "dark"; - } + var theme = saved === "light" ? "light" : "dark"; root.classList.add("js"); root.dataset.theme = theme; + + var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (!reduce) { + root.classList.add("motion"); + window.setTimeout(function () { + if (!root.classList.contains("motion-ready")) root.classList.remove("motion"); + }, 3000); + } + var apply = function () { document.body.classList.toggle("theme-dark", theme === "dark"); }; if (document.body) apply(); else document.addEventListener("DOMContentLoaded", apply); diff --git a/docs/js/vendor/LICENSE-lenis.txt b/docs/js/vendor/LICENSE-lenis.txt new file mode 100644 index 0000000..4960877 --- /dev/null +++ b/docs/js/vendor/LICENSE-lenis.txt @@ -0,0 +1,9 @@ +The MIT License + +Copyright (c) 2024 darkroom.engineering + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/js/vendor/README.md b/docs/js/vendor/README.md new file mode 100644 index 0000000..c24c526 --- /dev/null +++ b/docs/js/vendor/README.md @@ -0,0 +1,15 @@ +# 第三方动画库 + +官网动画用到的库,原样从 npm 包的 `dist/` 复制过来,只去掉了 sourceMappingURL 注释。 +放在本地而不是 CDN,是为了国内访问稳定,也让页面不依赖外部服务。 +加载顺序由 `tools/sync_docs_chrome.py` 的 `VENDOR_SCRIPTS` 决定。 + +| 文件 | 来源 | 版本 | 许可 | +|---|---|---|---| +| `gsap.min.js` | npm `gsap` | 3.15.1 | GSAP Standard "No Charge" License, | +| `ScrollTrigger.min.js` | npm `gsap` | 3.15.1 | 同上 | +| `SplitText.min.js` | npm `gsap` | 3.15.1 | 同上 | +| `lenis.min.js` | npm `lenis` | 1.3.27 | MIT,见 `LICENSE-lenis.txt` | + +升级时:`npm pack gsap lenis`,解包后把同名文件覆盖进来,删掉末尾的 sourceMappingURL 注释, +更新上表,再运行 `python tools/sync_docs_chrome.py` 刷新各页面引用里的版本哈希。 diff --git a/docs/js/vendor/ScrollTrigger.min.js b/docs/js/vendor/ScrollTrigger.min.js new file mode 100644 index 0000000..4c89ae3 --- /dev/null +++ b/docs/js/vendor/ScrollTrigger.min.js @@ -0,0 +1,11 @@ +/*! + * ScrollTrigger 3.15.1 + * https://gsap.com + * + * @license Copyright 2026, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r=Math.abs(r)?t:r}function P(){(Ae=Se.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,Ie),n.push.apply(n,Le),Ie=t,Le=n,o=function _bridge(e,t){return r[e](t)}}()}function Q(e){return Se=e||r(),!Te&&Se&&"undefined"!=typeof document&&document.body&&(Ce=window,ke=(Me=document).documentElement,Ee=Me.body,t=[Ce,Me,ke,Ee],Se.utils.clamp,Be=Se.core.context||function(){},Oe="onpointerenter"in Ee?"pointer":"mouse",Pe=k.isTouch=Ce.matchMedia&&Ce.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Ce||0=i,n=Math.abs(t)>=i;T&&(r||n)&&T(se,e,t,me,ye),r&&(m&&0Math.abs(t)?"x":"y",oe=!0),"y"!==ae&&(me[2]+=e,se._vx.update(e,!0)),"x"!==ae&&(ye[2]+=t,se._vy.update(t,!0)),n?ee=ee||requestAnimationFrame(lf):lf()}function of(e){if(!jf(e,1)){var t=(e=N(e,s)).clientX,r=e.clientY,n=t-se.x,i=r-se.y,o=se.isDragging;se.x=t,se.y=r,(o||(n||i)&&(Math.abs(se.startX-t)>=a||Math.abs(se.startY-r)>=a))&&(re=re||(o?2:1),o||(se.isDragging=!0),nf(n,i))}}function rf(e){return e.touches&&1=e)return a[n];return a[n-1]}for(n=a.length,e+=r;n--;)if(a[n]<=e)return a[n];return a[0]}:function(e,t,r){void 0===r&&(r=.001);var n=o(e);return!t||Math.abs(n-e)r&&(n*=t/100),e=e.substr(0,r-1)),e=n+(e in X?X[e]*t:~e.indexOf("%")?parseFloat(e)*t/100:parseFloat(e)||0)}return e}function Eb(e,t,r,n,i,o,a,s){var l=i.startColor,c=i.endColor,u=i.fontSize,f=i.indent,d=i.fontWeight,p=Ue.createElement("div"),g=Ma(r)||"fixed"===z(r,"pinType"),h=-1!==e.indexOf("scroller"),v=g?He:"IFRAME"===r.tagName?r.contentDocument.body:r,b=-1!==e.indexOf("start"),m=b?l:c,y="border-color:"+m+";font-size:"+u+";color:"+m+";font-weight:"+d+";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";return y+="position:"+((h||s)&&g?"fixed;":"absolute;"),!h&&!s&&g||(y+=(n===Xe?D:I)+":"+(o+parseFloat(f))+"px;"),a&&(y+="box-sizing:border-box;text-align:left;width:"+a.offsetWidth+"px;"),p._isStart=b,p.setAttribute("class","gsap-marker-"+e+(t?" marker-"+t:"")),p.style.cssText=y,p.innerText=t||0===t?e+"-"+t:e,v.children[0]?v.insertBefore(p,v.children[0]):v.appendChild(p),p._offset=p["offset"+n.op.d2],U(p,0,n,b),p}function Jb(){return 34He.clientWidth)||(Ie.cache++,v?T=T||requestAnimationFrame(K):K(),lt||W("scrollStart"),lt=st())}function Lb(){y=Je.innerWidth,m=Je.innerHeight}function Mb(e){Ie.cache++,!0!==e&&(je||h||Ue.fullscreenElement||Ue.webkitFullscreenElement||b&&y===Je.innerWidth&&!(Math.abs(Je.innerHeight-m)>.25*Je.innerHeight))||c.restart(!0)}function Pb(){return yb(re,"scrollEnd",Pb)||Et(!0)}function Sb(e){for(var t=0;tt,n=e._startClamp&&e.start>=t;(r||n)&&e.setPositions(n?t-1:e.start,r?Math.max(n?t:e.start+1,t):e.end,!0)}),_b(!1),tt=0,r.forEach(function(e){return e&&e.render&&e.render(-1)}),Ie.forEach(function(e){Ua(e)&&(e.smooth&&requestAnimationFrame(function(){return e.target.style.scrollBehavior="smooth"}),e.rec&&e(e.rec))}),Vb(_,1),c.pause(),kt++,K(nt=2),Ct.forEach(function(e){return Ua(e.vars.onRefresh)&&e.vars.onRefresh(e)}),nt=re.isRefreshing=!1,W("refresh")}else xb(re,"scrollEnd",Pb)},j=0,Pt=1,K=function _updateAll(e){if(2===e||!nt&&!et){re.isUpdating=!0,ot&&ot.update(0);var t=Ct.length,r=st(),n=50<=r-R,i=t&&Ct[0].scroll();if(Pt=i=Ra(be,he)){if(oe&&Ae()&&!de)for(o=oe.parentNode;o&&o!==He;)o._pinOffset&&(B-=o._pinOffset,I-=o._pinOffset),o=o.parentNode}else i=nb(ae),s=he===Xe,a=Ae(),K=parseFloat(j(he.a))+w,!y&&1=I})},Te.update=function(e,t,r){if(!de||r||e){var n,i,o,a,s,l,c,u=!0===nt?re:Te.scroll(),f=e?0:(u-B)/q,d=f<0?0:1u+(u-D)/(st()-Ge)*E&&(d=.9999)),d!==p&&Te.enabled){if(a=(s=(n=Te.isActive=!!d&&d<1)!=(!!p&&p<1))||!!d!=!!p,Te.direction=p=Ra(be,he),fe)if(e||!n&&!l)qc(ae,G);else{var g=wt(ae,!0),h=u-B;qc(ae,He,g.top+(he===Xe?h:0)+_t,g.left+(he===Xe?0:h)+_t)}Ot(n||l?W:V),Z&&d<1&&n||b(K+(1!==d||l?0:$))}}else b(Ja(K+$*d));!ue||A.tween||je||at||te.restart(!0),C&&(s||ce&&d&&(d<1||!rt))&&We(C.targets).forEach(function(e){return e.classList[n||ce?"add":"remove"](C.className)}),!T||ve||e||T(Te),a&&!je?(ve&&(c&&("complete"===o?O.pause().totalProgress(1):"reset"===o?O.restart(!0).pause():"restart"===o?O.restart(!0):O[o]()),T&&T(Te)),!s&&rt||(M&&s&&Ya(Te,M),xe[i]&&Ya(Te,xe[i]),ce&&(1===d?Te.kill(!1,1):xe[i]=0),s||xe[i=1===d?1:3]&&Ya(Te,xe[i])),pe&&!n&&Math.abs(Te.getVelocity())>(Va(pe)?pe:2500)&&(Xa(Te.callbackAnimation),ee?ee.progress(1):Xa(O,"reverse"===o?1:!d,1))):ve&&T&&!je&&T(Te)}if(x){var v=de?u/de.duration()*(de._caScrollDist||0):u;y(v+(X._isFlipped?1:0)),x(v)}S&&S(-u/de.duration()*(de._caScrollDist||0))}},Te.enable=function(e,t){Te.enabled||(Te.enabled=!0,xb(be,"resize",Mb),me||xb(be,"scroll",Kb),Ce&&xb(ScrollTrigger,"refreshInit",Ce),!1!==e&&(Te.progress=Oe=0,R=D=Ee=Ae()),!1!==t&&Te.refresh())},Te.getTween=function(e){return e&&A?A.tween:ee},Te.setPositions=function(e,t,r,n){if(de){var i=de.scrollTrigger,o=de.duration(),a=i.end-i.start;e=i.start+a*e/o,t=i.start+a*t/o}Te.refresh(!1,!1,{start:Ea(e,r&&!!Te._startClamp),end:Ea(t,r&&!!Te._endClamp)},n),Te.update()},Te.adjustPinSpacing=function(e){if(Q&&e){var t=Q.indexOf(he.d)+1;Q[t]=parseFloat(Q[t])+e+_t,Q[1]=parseFloat(Q[1])+e+_t,Ot(Q)}},Te.disable=function(e,t){if(!1!==e&&Te.revert(!0,!0),Te.enabled&&(Te.enabled=Te.isActive=!1,t||ee&&ee.pause(),re=0,n&&(n.uncache=1),Ce&&yb(ScrollTrigger,"refreshInit",Ce),te&&(te.pause(),A.tween&&A.tween.kill()&&(A.tween=0)),!me)){for(var r=Ct.length;r--;)if(Ct[r].scroller===be&&Ct[r]!==Te)return;yb(be,"resize",Mb),me||yb(be,"scroll",Kb)}},Te.kill=function(e,t){Te.disable(e,t),ee&&!t&&ee.kill(),a&&delete Mt[a];var r=Ct.indexOf(Te);0<=r&&Ct.splice(r,1),r===$e&&0o&&(b()>o?a.progress(1)&&b(o):a.resetTo("scrollY",o))}Wa(e)||(e={}),e.preventDefault=e.isNormalizer=e.allowClicks=!0,e.type||(e.type="wheel,touch"),e.debounce=!!e.debounce,e.id=e.id||"normalizer";var n,o,l,i,a,c,u,s,f=e.normalizeScrollX,t=e.momentum,r=e.allowNestedScroll,d=e.onRelease,p=J(e.target)||qe,g=Fe.core.globals().ScrollSmoother,h=g&&g.get(),v=E&&(e.content&&J(e.content)||h&&!1!==e.content&&!h.smooth()&&h.content()),b=L(p,Xe),m=L(p,Ne),y=1,x=(k.isTouch&&Je.visualViewport?Je.visualViewport.scale*Je.visualViewport.width:Je.outerWidth)/Je.innerWidth,_=0,w=Ua(t)?function(){return t(n)}:function(){return t||2.8},S=zc(p,e.type,!0,r),T=Ia,C=Ia;return v&&Fe.set(v,{y:"+=0"}),e.ignoreCheck=function(e){return E&&"touchmove"===e.type&&function ignoreDrag(){if(i){requestAnimationFrame(Jq);var e=Ja(n.deltaY/2),t=C(b.v-e);if(v&&t!==b.v+b.offset){b.offset=t-b.v;var r=Ja((parseFloat(v&&v._gsap.y)||0)-b.offset);v.style.transform="matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, "+r+", 0, 1)",v._gsap.y=r+"px",b.cacheID=Ie.cache,K()}return!0}b.offset&&Nq(),i=!0}()||1.05=o||o-1<=r)&&Fe.to({},{onUpdate:Tq,duration:i})}else s.restart(!0);d&&d(e)},e.onWheel=function(){a._ts&&a.pause(),1e3e?"string"==typeof e?s(document.querySelectorAll(e)):"length"in e?Array.from(e).reduce((e,t)=>("string"==typeof t?e.push(...s(t)):e.push(t),e),[]):[e]:[],o=e=>s(e).filter(e=>e&&1===e.nodeType),a=[],h=function(){},d={add:e=>e()},p=/\s+/g,c=new RegExp("\\p{RI}\\p{RI}|\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?(\\u{200D}\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)*|.","gu"),u={left:0,top:0,width:0,height:0},f=(e,t)=>{for(;++t{e.innerHTML=t,i?e.setAttribute("aria-label",i):e.removeAttribute("aria-label"),n?e.setAttribute("aria-hidden",n):e.removeAttribute("aria-hidden")},g=(e,t)=>{if(t){let i,n,l,r,s=new Set(e.join("").match(t)||a),o=e.length;if(s.size)for(;--o>-1;)for(l of(n=e[o],s))if(l.startsWith(n)&&l.length>n.length){for(i=0,r=n;l.startsWith(r+=e[o+ ++i])&&r.length"inline"===window.getComputedStyle(e).display&&(e.style.display="inline-block"),y=(e,t,i)=>t.insertBefore("string"==typeof e?document.createTextNode(e):e,i),x=(e,t,i)=>{let n=t[e+"sClass"]||"",{tag:l="div",aria:r="auto",propIndex:s=!1}=t,o="line"===e?"block":"inline-block",a=n.indexOf("++")>-1,h=t=>{let h=document.createElement(l),d=i.length+1;return n&&(h.className=n+(a?" "+n+d:"")),s&&h.style.setProperty("--"+e,d+""),"none"!==r&&h.setAttribute("aria-hidden","true"),"span"!==l&&(h.style.position="relative",h.style.display=o),h.textContent=t,i.push(h),h};return a&&(n=n.replace("++","")),h.collection=i,h},b=(e,t,i,n,l,s,o,h,d,c)=>{var u;let f,m,x,v,C,E,S,T,A,N,_,R,k,O,W,B,j,F,I=Array.from(e.childNodes),L=0,{wordDelimiter:z,reduceWhiteSpace:M=!0,prepareText:H}=t,D=e.getBoundingClientRect(),P=D,q=!M&&"pre"===window.getComputedStyle(e).whiteSpace.substring(0,3),G=s&&c&&"rtl"===window.getComputedStyle(e).direction,J=0,K=i.collection;for("object"==typeof z?(x=z.delimiter||z,m=z.replaceWith||""):m=""===z?"":z||" ",f=" "!==m;L-1?(E=K[K.length-1],E.appendChild(document.createTextNode(n?"":B))):(E=i(n?"":B),y(E,e,v),J&&1===A&&!S&&E.insertBefore(J,E.firstChild)),n){if(_=r?g([...r.segment(B)].map(e=>e.segment),d):B.match(h)||a,d){let e=B.match(d);e&&e.some(e=>_.indexOf(e)<0)&&(_=B.match(h)||a)}for(F=0;F<_.length;F++)E.appendChild(" "===_[F]?document.createTextNode(" "):n(_[F]))}if(s&&c){if(W=v.textContent=W.substring(B.length+1,W.length),N=E.getBoundingClientRect(),N.top>P.top&&(G?N.left>=P.left:N.left<=P.left)){for(R=e.cloneNode(),k=e.childNodes[0];k&&k!==E;)O=k,k=k.nextSibling,R.appendChild(O);e.parentNode.insertBefore(R,e),l&&w(R)}P=N}(A=C.length?" ":f&&" "===B.slice(-1)?" "+m:m,e,v)}else y(m,e,v);e.removeChild(v),J=0}else 1===v.nodeType&&(o&&o.indexOf(v)>-1?(K.indexOf(v.previousSibling)>-1&&K[K.length-1].appendChild(v),J=v):(b(v,t,i,n,l,s,o,h,d,!0),J=0),l&&w(v))};const v=class e{constructor(e,t){this.isSplit=!1,n||C.register(window.gsap),this.elements=o(e),this.chars=[],this.words=[],this.lines=[],this.masks=[],this.vars=t,this.elements.forEach(e=>{var i;!1!==t.overwrite&&(null==(i=e[l])||i._data.orig.filter(({element:t})=>t===e).forEach(m)),e[l]=this}),this._split=()=>this.isSplit&&this.split(this.vars);let i=[],r=()=>{let e,t=i.length;for(;t--;){e=i[t];let n=e.element.offsetWidth;if(n!==e.width)return e.width=n,void this._split()}};this._data={orig:i,obs:"undefined"!=typeof ResizeObserver&&new ResizeObserver(()=>{clearTimeout(this._data.t),this._data.t=setTimeout(r,200)})},h(this),this.split(t)}split(e){return(this._ctx||d).add(()=>{this.isSplit&&this.revert(),this.vars=e=e||this.vars||{};let t,{type:n="chars,words,lines",aria:l="auto",deepSlice:r=!0,smartWrap:a,onSplit:h,autoSplit:d=!1,specialChars:p,mask:m}=this.vars,g=n.indexOf("lines")>-1,w=n.indexOf("chars")>-1,y=n.indexOf("words")>-1,v=w&&!y&&!g,C=p&&("push"in p?new RegExp("(?:"+p.join("|")+")","gu"):p),E=C?new RegExp(C.source+"|"+c.source,"gu"):c,S=!!e.ignore&&o(e.ignore),{orig:T,animTime:A,obs:N}=this._data;(w||y||g)&&(this.elements.forEach((t,i)=>{T[i]={element:t,html:t.innerHTML,ariaL:t.getAttribute("aria-label"),ariaH:t.getAttribute("aria-hidden")},"auto"===l?t.setAttribute("aria-label",(t.textContent||"").trim()):"hidden"===l&&t.setAttribute("aria-hidden","true");let n,o,h,d,p=[],c=[],m=[],A=w?x("char",e,p):null,N=x("word",e,c);if(b(t,e,N,A,v,r&&(g||v),S,E,C,!1),g){let i,l,r=s(t.childNodes),o=((e,t,i,n)=>{let l=x("line",i,n),r=window.getComputedStyle(e).textAlign||"left";return(i,n)=>{let s=l("");for(s.style.textAlign=r,e.insertBefore(s,t[i]);i1===e.nodeType?e.getBoundingClientRect():u),c=u;for(n=0;nc.top&&(d?l.left+l.width>c.left+1:l.left{var t;return null==(t=e.parentNode)?void 0:t.removeChild(e)})}if(!y){for(n=0;n{let t=e.cloneNode();return e.replaceWith(t),t.appendChild(e),e.className&&(t.className=e.className.trim().split(" ").map(e=>e+"-mask").join(" ")),t.style.overflow="clip",t.style.verticalAlign="top",t}))),this.isSplit=!0,i&&g&&d&&i.addEventListener("loadingdone",this._split),(t=h&&h(this))&&t.totalTime&&(this._data.anim=A?t.totalTime(A):t),g&&d&&this.elements.forEach((e,t)=>{T[t].width=e.offsetWidth,N&&N.observe(e)})}),this}kill(){let{obs:e,t:t}=this._data;e&&e.disconnect(),clearTimeout(t),null==i||i.removeEventListener("loadingdone",this._split)}revert(){var e,t;if(this.isSplit){let{orig:i,anim:n}=this._data;this.kill(),i.forEach(m),this.chars.length=this.words.length=this.lines.length=i.length=this.masks.length=0,this.isSplit=!1,n&&(this._data.animTime=n.totalTime(),n.revert()),null==(t=(e=this.vars).onRevert)||t.call(e,this)}return this}static create(t,i){return new e(t,i)}static register(e){t=t||e||window.gsap,t&&(s=t.utils.toArray,h=t.core.context||h),!n&&window.innerWidth>0&&(i=document.fonts,n=!0)}};v.version="3.15.1";let C=v;e.SplitText=C,e.default=C;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}})); + diff --git a/docs/js/vendor/gsap.min.js b/docs/js/vendor/gsap.min.js new file mode 100644 index 0000000..c6d5f85 --- /dev/null +++ b/docs/js/vendor/gsap.min.js @@ -0,0 +1,11 @@ +/*! + * GSAP 3.15.1 + * https://gsap.com + * + * @license Copyright 2026, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new qt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0X)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur=e._time)?e._last?e._last._start:e._time:i||e!==I?Ot(e,i,r):e._time)+r._delay),r._end=la(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),Aa(e,r,"_first","_last",e._sort?"_start":0),xt(r)||(e._recent=r),n||Ma(e,r),e._ts<0&&La(e,e._tTime),e}function Oa(t,e){return(ht.ScrollTrigger||S("scrollTrigger",e))&&ht.ScrollTrigger.create(e,t)}function Pa(t,e,r,i,n){return $t(t,e,n),t._initted?!r&&t._pt&&!L&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==Lt.frame?(pt.push(t),t._lazy=[n,i],1):void 0:1}function Ua(t,e,r,i){var n=t._repeat,a=la(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:la(a*(n+1)+t._rDelay*n):a,0(n=Math.abs(n))&&(a=i,o=n);return a}function wb(t){return Ca(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&At(t,"onInterrupt"),t}function zb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:V,render:_e,add:Ht,kill:Te,modifier:ve,rawVars:0},a={targetTest:0,get:0,getSetter:ue,aliases:{},register:0};if(It(),t!==i){if(mt[e])return;ta(i,ta(xa(t,n),a)),bt(i.prototype,bt(n,xa(t,a))),mt[i.prop=e]=i,t.targetTest&&(yt.push(i),dt[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}U(e,i),t.register&&t.register(Fe,i,we)}else Dt.push(t)}function Cb(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&zt,e&zt]:0:Rt.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Rt[e])p=Rt[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&zt,p&zt,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&zt,e&zt]}else if("hsl"===e.substr(0,3))if(p=d=e.match(rt),r){if(~e.indexOf("="))return p=e.match(it),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=q?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*(K(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof te?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&Ba(this,t),t===this._recent&&(this._recent=this._last),Da(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=la(Lt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=Ot(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=te.to(i,ta({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||X,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ua(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ta({startAt:{time:Ot(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+X)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);var i,n=this._first,a=this.labels;for(t=la(t);n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Da(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Da(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=q;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Na(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=la(r/a._ts),a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ua(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(qa(I,Ja(t,I)),f=Lt.frame),Lt.frame>=vt){vt+=Y.autoSleep||120;var e=I._first;if((!e||!e._ts)&&Y.autoSleep&&Lt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Lt.sleep()}}},Timeline}(Xt);ta(Gt.prototype,{_lock:0,_hasPause:0,_forcing:0});function cc(t,e,i,n,a,o){var u,h,l,f;if(mt[t]&&!1!==(u=new mt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Qt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||K(t)||J(t))return r(t)?Qt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Qt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new we(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==c))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function ic(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if(K(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Zt,Wt,Ht=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var c,d=t[e],p="get"!==i?i:s(d)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():d,_=s(d)?l?se:ae:ie;if(r(n)&&(~n.indexOf("random(")&&(n=rb(n)),"="===n.charAt(1)&&(!(c=ma(p,n)+(_a(p)||0))&&0!==c||(n=c))),!f||p!==n||Wt)return isNaN(p*n)||""===n?(d||e in t||S(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,c,d,p,_=new we(this._pt,t,e,0,1,pe,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(d=~(i+="").indexOf("random("))&&(i=rb(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(at)||[];o=at.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(c=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:c,c:"="===l.charAt(1)?ma(c,l)-c:parseFloat(l)-c,m:h&&h<4?Math.round:0},m=at.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},k)"ease"===l||"easeEach"===l||ic(l,k[l],u,k.easeEach);for(l in u)for(A=u[l].sort(function(t,e){return t.t-e.t}),o=R=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&Ca(t,1),r||L||(At(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),E.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){d||Lt.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||$t(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,c=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!c)for(c=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return Wt=1,t.vars[e]="+=0",$t(t,s),Wt=0,o?T(e+" not eligible for reset. Try splitting into individual properties"):1;c.push(u)}for(f=c.length;f--;)(u=(h=c[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ka(r)+_a(h.e)),h.b&&(h.b=u.s+_a(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(La(this,0),this.parent||Aa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?wb(this):this.scrollTrigger&&this.scrollTrigger.kill(!!L),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Zt&&!0!==Zt.vars.overwrite)._first||wb(this),this.parent&&i!==this.timeline.totalDuration()&&Ua(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,c=t?Pt(t):f,d=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,c))return"all"===e&&(this._pt=0),wb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ja(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?ha(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=bt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~c.indexOf(f[l]))for(u in a=d[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||Ba(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&wb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Ya(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Ya(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Xt);ta(te.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ja("staggerTo,staggerFrom,staggerFromTo",function(r){te[r]=function(){var t=new Gt,e=Ct.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function qc(t,e,r){return t.setAttribute(e,r)}function yc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var ie=function _setterPlain(t,e,r){return t[e]=r},ae=function _setterFunc(t,e,r){return t[e](r)},se=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ue=function _getSetter(t,e){return s(t[e])?ae:u(t[e])&&t.setAttribute?qc:ie},fe=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},de=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},pe=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},_e=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},ve=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},Te=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?Ba(this,i,"_pt"):i.dep||(e=1),i=r;return!e},be=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},we=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=yc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||fe,this.d=s||this,this.set=o||ie,this.pr=u||0,(this._next=t)&&(t._prev=this)}ja(Tt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger,easeReverse",function(t){return dt[t]=1}),ht.TweenMax=ht.TweenLite=te,ht.TimelineLite=ht.TimelineMax=Gt,I=new Gt({sortChildren:!1,defaults:j,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),Y.stringFilter=Ib;function Gc(t){return(Oe[t]||Me).map(function(t){return t()})}function Hc(){var t=Date.now(),o=[];2=1;let i=n?1:this.easing(r);this.value=this.from+(this.to-this.from)*i}else this.lerp?(this.value=r(this.value,this.to,this.lerp*60,e),Math.round(this.value)===Math.round(this.to)&&(this.value=this.to,n=!0)):(this.value=this.to,n=!0);n&&this.stop(),this.onUpdate?.(this.value,n)}stop(){this.isRunning=!1}fromTo(e,t,{lerp:n,duration:r,easing:i,onStart:a,onUpdate:o}){this.from=this.value=e,this.to=t,this.lerp=n,this.duration=r,this.easing=i,this.currentTime=0,this.isRunning=!0,a?.(),this.onUpdate=o}};function o(e,t){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>{n=void 0,e.apply(this,r)},t)}}var s=class{width=0;height=0;scrollHeight=0;scrollWidth=0;debouncedResize;wrapperResizeObserver;contentResizeObserver;constructor(e,t,{autoResize:n=!0,debounce:r=250}={}){this.wrapper=e,this.content=t,n&&(this.debouncedResize=o(this.resize,r),this.wrapper instanceof Window?window.addEventListener(`resize`,this.debouncedResize):(this.wrapperResizeObserver=new ResizeObserver(this.debouncedResize),this.wrapperResizeObserver.observe(this.wrapper)),this.contentResizeObserver=new ResizeObserver(this.debouncedResize),this.contentResizeObserver.observe(this.content)),this.resize()}destroy(){this.wrapperResizeObserver?.disconnect(),this.contentResizeObserver?.disconnect(),this.wrapper===window&&this.debouncedResize&&window.removeEventListener(`resize`,this.debouncedResize)}resize=()=>{this.onWrapperResize(),this.onContentResize()};onWrapperResize=()=>{this.wrapper instanceof Window?(this.width=window.innerWidth,this.height=window.innerHeight):(this.width=this.wrapper.clientWidth,this.height=this.wrapper.clientHeight)};onContentResize=()=>{this.wrapper instanceof Window?(this.scrollHeight=this.content.scrollHeight,this.scrollWidth=this.content.scrollWidth):(this.scrollHeight=this.wrapper.scrollHeight,this.scrollWidth=this.wrapper.scrollWidth)};get limit(){return{x:this.scrollWidth-this.width,y:this.scrollHeight-this.height}}},c=class{events={};emit(e,...t){let n=this.events[e]||[];for(let e=0,r=n.length;e{this.events[e]=this.events[e]?.filter(e=>t!==e)}}off(e,t){this.events[e]=this.events[e]?.filter(e=>t!==e)}destroy(){this.events={}}};let l={passive:!1};function u(e,t){return e===1?16.666666666666668:e===2?t:1}var d=class{touchStart={x:0,y:0};lastDelta={x:0,y:0};window={width:0,height:0};emitter=new c;constructor(e,t={wheelMultiplier:1,touchMultiplier:1}){this.element=e,this.options=t,window.addEventListener(`resize`,this.onWindowResize),this.onWindowResize(),this.element.addEventListener(`wheel`,this.onWheel,l),this.element.addEventListener(`touchstart`,this.onTouchStart,l),this.element.addEventListener(`touchmove`,this.onTouchMove,l),this.element.addEventListener(`touchend`,this.onTouchEnd,l)}on(e,t){return this.emitter.on(e,t)}destroy(){this.emitter.destroy(),window.removeEventListener(`resize`,this.onWindowResize),this.element.removeEventListener(`wheel`,this.onWheel,l),this.element.removeEventListener(`touchstart`,this.onTouchStart,l),this.element.removeEventListener(`touchmove`,this.onTouchMove,l),this.element.removeEventListener(`touchend`,this.onTouchEnd,l)}onTouchStart=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:0,y:0},this.emitter.emit(`scroll`,{deltaX:0,deltaY:0,event:e})};onTouchMove=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e,r=-(t-this.touchStart.x)*this.options.touchMultiplier,i=-(n-this.touchStart.y)*this.options.touchMultiplier;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:r,y:i},this.emitter.emit(`scroll`,{deltaX:r,deltaY:i,event:e})};onTouchEnd=e=>{this.emitter.emit(`scroll`,{deltaX:this.lastDelta.x,deltaY:this.lastDelta.y,event:e})};onWheel=e=>{let{deltaX:t,deltaY:n,deltaMode:r}=e,i=u(r,this.window.width),a=u(r,this.window.height);t*=i,n*=a,t*=this.options.wheelMultiplier,n*=this.options.wheelMultiplier,this.emitter.emit(`scroll`,{deltaX:t,deltaY:n,event:e})};onWindowResize=()=>{this.window={width:window.innerWidth,height:window.innerHeight}}};let f=e=>Math.min(1,1.001-2**(-10*e));var p=class{_isScrolling=!1;_isStopped=!1;_isLocked=!1;_preventNextNativeScrollEvent=!1;_resetVelocityTimeout=null;_rafId=null;_isDraggingSelection=!1;reducedMotionMediaQuery=window.matchMedia(`(prefers-reduced-motion: reduce)`);isTouching;isIos;time=0;userData={};lastVelocity=0;velocity=0;direction=0;options;targetScroll;animatedScroll;animate=new a;emitter=new c;dimensions;virtualScroll;constructor({wrapper:t=window,content:n=document.documentElement,eventsTarget:r=t,smoothWheel:i=!0,syncTouch:a=!1,syncTouchLerp:o=.075,touchInertiaExponent:c=1.7,duration:l,easing:u,lerp:p=.1,infinite:m=!1,orientation:h=`vertical`,gestureOrientation:g=h===`horizontal`?`both`:`vertical`,touchMultiplier:_=1,wheelMultiplier:v=1,autoResize:y=!0,prevent:b,virtualScroll:x,overscroll:S=!0,autoRaf:C=!1,anchors:w=!1,autoToggle:T=!1,allowNestedScroll:E=!1,__experimental__naiveDimensions:D=!1,naiveDimensions:O=D,stopInertiaOnNavigate:k=!1,respectReducedMotion:A=!0}={}){window.lenisVersion=e,window.lenis||(window.lenis={}),window.lenis.version=e,h===`horizontal`&&(window.lenis.horizontal=!0),a===!0&&(window.lenis.touch=!0),this.isIos=/(iPad|iPhone|iPod)/g.test(navigator.userAgent),(!t||t===document.documentElement)&&(t=window),typeof l==`number`&&typeof u!=`function`?u=f:typeof u==`function`&&typeof l!=`number`&&(l=1),this.options={wrapper:t,content:n,eventsTarget:r,smoothWheel:i,syncTouch:a,syncTouchLerp:o,touchInertiaExponent:c,duration:l,easing:u,lerp:p,infinite:m,gestureOrientation:g,orientation:h,touchMultiplier:_,wheelMultiplier:v,autoResize:y,prevent:b,virtualScroll:x,overscroll:S,autoRaf:C,anchors:w,autoToggle:T,allowNestedScroll:E,naiveDimensions:O,stopInertiaOnNavigate:k,respectReducedMotion:A},this.dimensions=new s(t,n,{autoResize:y}),this.updateClassName(),this.targetScroll=this.animatedScroll=this.actualScroll,this.options.wrapper.addEventListener(`scroll`,this.onNativeScroll),this.options.wrapper.addEventListener(`scrollend`,this.onScrollEnd,{capture:!0}),(this.options.anchors||this.options.stopInertiaOnNavigate)&&this.options.wrapper.addEventListener(`click`,this.onClick),this.options.wrapper.addEventListener(`pointerdown`,this.onPointerDown),this.virtualScroll=new d(r,{touchMultiplier:_,wheelMultiplier:v}),this.virtualScroll.on(`scroll`,this.onVirtualScroll),this.options.autoToggle&&(this.checkOverflow(),this.rootElement.addEventListener(`transitionend`,this.onTransitionEnd)),this.options.autoRaf&&(this._rafId=requestAnimationFrame(this.raf))}destroy(){this.emitter.destroy(),this._resetVelocityTimeout!==null&&(clearTimeout(this._resetVelocityTimeout),this._resetVelocityTimeout=null),this.options.wrapper.removeEventListener(`scroll`,this.onNativeScroll),this.options.wrapper.removeEventListener(`scrollend`,this.onScrollEnd,{capture:!0}),this.options.wrapper.removeEventListener(`pointerdown`,this.onPointerDown),(this.options.anchors||this.options.stopInertiaOnNavigate)&&this.options.wrapper.removeEventListener(`click`,this.onClick),this.virtualScroll.destroy(),this.dimensions.destroy(),this.cleanUpClassName(),this._rafId&&cancelAnimationFrame(this._rafId)}on(e,t){return this.emitter.on(e,t)}off(e,t){return this.emitter.off(e,t)}onScrollEnd=e=>{e instanceof CustomEvent||(this.isScrolling===`smooth`||this.isScrolling===!1)&&e.stopPropagation()};dispatchScrollendEvent=()=>{this.options.wrapper.dispatchEvent(new CustomEvent(`scrollend`,{bubbles:this.options.wrapper===window,detail:{lenisScrollEnd:!0}}))};get overflow(){let e=this.isHorizontal?`overflow-x`:`overflow-y`;return getComputedStyle(this.rootElement)[e]}checkOverflow(){[`hidden`,`clip`].includes(this.overflow)?this.internalStop():this.internalStart()}onTransitionEnd=e=>{e.propertyName?.includes(`overflow`)&&e.target===this.rootElement&&this.checkOverflow()};setScroll(e){this.isHorizontal?this.options.wrapper.scrollTo({left:e,behavior:`instant`}):this.options.wrapper.scrollTo({top:e,behavior:`instant`})}onClick=e=>{let t=e.composedPath().filter(e=>e instanceof HTMLAnchorElement&&e.href).map(e=>new URL(e.href)),n=new URL(window.location.href);if(this.options.anchors){let e=t.find(e=>n.host===e.host&&n.pathname===e.pathname&&e.hash);if(e){let t=typeof this.options.anchors==`object`&&this.options.anchors?this.options.anchors:void 0,n=decodeURIComponent(e.hash);this.scrollTo(n,t);return}}if(this.options.stopInertiaOnNavigate&&t.some(e=>n.host===e.host&&n.pathname!==e.pathname)){this.reset();return}};onPointerDown=e=>{e.button===1&&this.reset()};isTouchOnSelectionHandle(e){let t=window.getSelection();if(!t||t.isCollapsed||t.rangeCount===0)return!1;let n=e.targetTouches[0]??e.changedTouches[0];if(!n)return!1;let r=t.getRangeAt(0).getClientRects();if(r.length===0)return!1;let i=r[0],a=r[r.length-1],o=Math.hypot(n.clientX-i.left,n.clientY-i.top)<=40,s=Math.hypot(n.clientX-a.right,n.clientY-a.bottom)<=40;return o||s}onVirtualScroll=e=>{if(typeof this.options.virtualScroll==`function`&&this.options.virtualScroll(e)===!1)return;let{deltaX:t,deltaY:n,event:r}=e;if(this.emitter.emit(`virtual-scroll`,{deltaX:t,deltaY:n,event:r}),r.ctrlKey||r.lenisStopPropagation)return;let i=r.type.includes(`touch`),a=r.type.includes(`wheel`);if(i&&this.isIos&&(r.type===`touchstart`&&(this._isDraggingSelection=this.isTouchOnSelectionHandle(r)),this._isDraggingSelection)){r.type===`touchend`&&(this._isDraggingSelection=!1);return}this.isTouching=r.type===`touchstart`||r.type===`touchmove`;let o=t===0&&n===0;if(this.options.syncTouch&&i&&r.type===`touchstart`&&o&&!this.isStopped&&!this.isLocked){this.reset();return}let s=this.options.gestureOrientation===`vertical`&&n===0||this.options.gestureOrientation===`horizontal`&&t===0;if(o||s)return;let c=r.composedPath();c=c.slice(0,c.indexOf(this.rootElement));let l=this.options.prevent,u=Math.abs(t)>=Math.abs(n)?`horizontal`:`vertical`;if(c.find(e=>e instanceof HTMLElement&&(typeof l==`function`&&l?.(e)||e.hasAttribute?.(`data-lenis-prevent`)||u===`vertical`&&e.hasAttribute?.(`data-lenis-prevent-vertical`)||u===`horizontal`&&e.hasAttribute?.(`data-lenis-prevent-horizontal`)||i&&e.hasAttribute?.(`data-lenis-prevent-touch`)||a&&e.hasAttribute?.(`data-lenis-prevent-wheel`)||this.options.allowNestedScroll&&this.hasNestedScroll(e,{deltaX:t,deltaY:n}))))return;if(this.isStopped||this.isLocked){r.cancelable&&r.preventDefault();return}if(!(this.options.syncTouch&&i||this.options.smoothWheel&&a)){this.isScrolling=`native`,this.animate.stop(),r.lenisStopPropagation=!0;return}let d=n;this.options.gestureOrientation===`both`?d=Math.abs(n)>Math.abs(t)?n:t:this.options.gestureOrientation===`horizontal`&&(d=t),(!this.options.overscroll||this.options.infinite||this.options.wrapper!==window&&this.limit>0&&(this.animatedScroll>0&&this.animatedScroll0||this.animatedScroll===this.limit&&n<0))&&(r.lenisStopPropagation=!0),r.cancelable&&r.preventDefault();let f=i&&this.options.syncTouch,p=i&&r.type===`touchend`;p&&(d=Math.sign(d)*Math.abs(this.velocity)**this.options.touchInertiaExponent),this.scrollTo(this.targetScroll+d,{programmatic:!1,...f?{lerp:p?this.options.syncTouchLerp:1}:{lerp:this.options.lerp,duration:this.options.duration,easing:this.options.easing}})};resize(){this.dimensions.resize(),this.animatedScroll=this.targetScroll=this.actualScroll,this.emit()}emit(){this.emitter.emit(`scroll`,this)}onNativeScroll=()=>{if(this._resetVelocityTimeout!==null&&(clearTimeout(this._resetVelocityTimeout),this._resetVelocityTimeout=null),this._preventNextNativeScrollEvent){this._preventNextNativeScrollEvent=!1;return}if(this.isScrolling===!1||this.isScrolling===`native`){let e=this.animatedScroll;this.animatedScroll=this.targetScroll=this.actualScroll,this.lastVelocity=this.velocity,this.velocity=this.animatedScroll-e,this.direction=Math.sign(this.animatedScroll-e),this.isStopped||(this.isScrolling=`native`),this.emit(),this.velocity!==0&&(this._resetVelocityTimeout=setTimeout(()=>{this.lastVelocity=this.velocity,this.velocity=0,this.isScrolling=!1,this.emit()},400))}};reset(){this.isLocked=!1,this.isScrolling=!1,this.animatedScroll=this.targetScroll=this.actualScroll,this.lastVelocity=this.velocity=0,this.animate.stop()}start(){if(this.isStopped){if(this.options.autoToggle){this.rootElement.style.removeProperty(`overflow`);return}this.internalStart()}}internalStart(){this.isStopped&&(this.reset(),this.isStopped=!1,this.emit())}stop(){if(!this.isStopped){if(this.options.autoToggle){this.rootElement.style.setProperty(`overflow`,`clip`);return}this.internalStop()}}internalStop(){this.isStopped||(this.reset(),this.isStopped=!0,this.emit())}raf=e=>{let t=e-(this.time||e);this.time=e,this.animate.advance(t*.001),this.options.autoRaf&&(this._rafId=requestAnimationFrame(this.raf))};scrollTo(e,{offset:n=0,immediate:r=!1,lock:i=!1,programmatic:a=!0,lerp:o=a?this.options.lerp:void 0,duration:s=a?this.options.duration:void 0,easing:c=a?this.options.easing:void 0,onStart:l,onComplete:u,force:d=!1,userData:p}={}){if(this.prefersReducedMotion&&(a?r=!0:(o=1,s=void 0,c=void 0)),(this.isStopped||this.isLocked)&&!d)return;let m=e,h=n;if(typeof m==`string`&&[`top`,`left`,`start`,`#`].includes(m))m=0;else if(typeof m==`string`&&[`bottom`,`right`,`end`].includes(m))m=this.limit;else{let e=null;if(typeof m==`string`?(e=m.startsWith(`#`)?document.getElementById(m.slice(1)):document.querySelector(m),e||(m===`#top`?m=0:console.warn(`Lenis: Target not found`,m))):m instanceof Element&&(e=m),e){if(this.options.wrapper!==window){let e=this.rootElement.getBoundingClientRect();h-=this.isHorizontal?e.left:e.top}let t=e.getBoundingClientRect(),n=getComputedStyle(e),r=this.isHorizontal?Number.parseFloat(n.scrollMarginLeft):Number.parseFloat(n.scrollMarginTop),i=getComputedStyle(this.rootElement),a=this.isHorizontal?Number.parseFloat(i.scrollPaddingLeft):Number.parseFloat(i.scrollPaddingTop);m=(this.isHorizontal?t.left:t.top)+this.animatedScroll-(Number.isNaN(r)?0:r)-(Number.isNaN(a)?0:a)}}if(typeof m==`number`){if(m+=h,this.options.infinite){if(a){this.targetScroll=this.animatedScroll=this.scroll;let e=m-this.animatedScroll;e>this.limit/2?m-=this.limit:e<-this.limit/2&&(m+=this.limit)}}else m=t(0,m,this.limit);if(m===this.targetScroll){l?.(this),u?.(this);return}if(this.userData=p??{},r){this.animatedScroll=this.targetScroll=m,this.setScroll(this.scroll),this.reset(),this.preventNextNativeScrollEvent(),this.emit(),u?.(this),this.userData={},requestAnimationFrame(()=>{this.dispatchScrollendEvent()});return}a||(this.targetScroll=m),typeof s==`number`&&typeof c!=`function`?c=f:typeof c==`function`&&typeof s!=`number`&&(s=1),this.animate.fromTo(this.animatedScroll,m,{duration:s,easing:c,lerp:o,onStart:()=>{i&&(this.isLocked=!0),this.isScrolling=`smooth`,l?.(this)},onUpdate:(e,t)=>{this.isScrolling=`smooth`,this.lastVelocity=this.velocity,this.velocity=e-this.animatedScroll,this.direction=Math.sign(this.velocity),this.animatedScroll=e,this.setScroll(this.scroll),a&&(this.targetScroll=e),t||this.emit(),t&&(this.reset(),this.emit(),u?.(this),this.userData={},requestAnimationFrame(()=>{this.dispatchScrollendEvent()}),this.preventNextNativeScrollEvent())}})}}preventNextNativeScrollEvent(){this._preventNextNativeScrollEvent=!0,requestAnimationFrame(()=>{this._preventNextNativeScrollEvent=!1})}hasNestedScroll(e,{deltaX:t,deltaY:n}){let r=Date.now();e._lenis||={};let i=e._lenis,a,o,s,c,l,u,d,f,p,m;if(r-(i.time??0)>2e3){i.time=Date.now();let t=window.getComputedStyle(e);if(i.computedStyle=t,a=[`auto`,`overlay`,`scroll`].includes(t.overflowX),o=[`auto`,`overlay`,`scroll`].includes(t.overflowY),l=[`auto`].includes(t.overscrollBehaviorX),u=[`auto`].includes(t.overscrollBehaviorY),i.hasOverflowX=a,i.hasOverflowY=o,!(a||o))return!1;d=e.scrollWidth,f=e.scrollHeight,p=e.clientWidth,m=e.clientHeight,s=d>p,c=f>m,i.isScrollableX=s,i.isScrollableY=c,i.scrollWidth=d,i.scrollHeight=f,i.clientWidth=p,i.clientHeight=m,i.hasOverscrollBehaviorX=l,i.hasOverscrollBehaviorY=u}else s=i.isScrollableX,c=i.isScrollableY,a=i.hasOverflowX,o=i.hasOverflowY,d=i.scrollWidth,f=i.scrollHeight,p=i.clientWidth,m=i.clientHeight,l=i.hasOverscrollBehaviorX,u=i.hasOverscrollBehaviorY;if(!(a&&s||o&&c))return!1;let h=Math.abs(t)>=Math.abs(n)?`horizontal`:`vertical`,g,_,v,y,b,x;if(h===`horizontal`)g=Math.round(e.scrollLeft),_=d-p,v=t,y=a,b=s,x=l;else if(h===`vertical`)g=Math.round(e.scrollTop),_=f-m,v=n,y=o,b=c,x=u;else return!1;return!x&&(g>=_||g<=0)?!0:(v>0?g<_:g>0)&&y&&b}get rootElement(){return this.options.wrapper===window?document.documentElement:this.options.wrapper}get limit(){return this.options.naiveDimensions?this.isHorizontal?this.rootElement.scrollWidth-this.rootElement.clientWidth:this.rootElement.scrollHeight-this.rootElement.clientHeight:this.dimensions.limit[this.isHorizontal?`x`:`y`]}get isHorizontal(){return this.options.orientation===`horizontal`}get actualScroll(){let e=this.options.wrapper;return this.isHorizontal?e.scrollX??e.scrollLeft:e.scrollY??e.scrollTop}get scroll(){return this.options.infinite?i(this.animatedScroll,this.limit):this.animatedScroll}get progress(){return this.limit===0?1:this.scroll/this.limit}get isScrolling(){return this._isScrolling}set isScrolling(e){this._isScrolling!==e&&(this._isScrolling=e,this.updateClassName())}get isStopped(){return this._isStopped}set isStopped(e){this._isStopped!==e&&(this._isStopped=e,this.updateClassName())}get isLocked(){return this._isLocked}set isLocked(e){this._isLocked!==e&&(this._isLocked=e,this.updateClassName())}get isSmooth(){return this.isScrolling===`smooth`}get prefersReducedMotion(){return this.options.respectReducedMotion&&this.reducedMotionMediaQuery.matches}get className(){let e=`lenis`;return this.options.autoToggle&&(e+=` lenis-autoToggle`),this.isStopped&&(e+=` lenis-stopped`),this.isLocked&&(e+=` lenis-locked`),this.isScrolling&&(e+=` lenis-scrolling`),this.isScrolling===`smooth`&&(e+=` lenis-smooth`),e}updateClassName(){this.cleanUpClassName(),this.className.split(` `).forEach(e=>{this.rootElement.classList.add(e)})}cleanUpClassName(){for(let e of Array.from(this.rootElement.classList))(e===`lenis`||e.startsWith(`lenis-`))&&this.rootElement.classList.remove(e)}};globalThis.Lenis=p,globalThis.Lenis.prototype=p.prototype})(); diff --git a/docs/ota/index.html b/docs/ota/index.html index 6367492..dbfe393 100644 --- a/docs/ota/index.html +++ b/docs/ota/index.html @@ -6,14 +6,14 @@ PySpOS 更新包 | GoutouStdio - - + + - +
- + + + + + + + - + + + + + + + - + + + + + + diff --git a/docs/requirements.html b/docs/requirements.html index 42d0f09..4500231 100644 --- a/docs/requirements.html +++ b/docs/requirements.html @@ -6,14 +6,14 @@ 配置要求 | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos6.html b/docs/spaceos6.html index 5ef6cfa..8b84347 100644 --- a/docs/spaceos6.html +++ b/docs/spaceos6.html @@ -6,14 +6,14 @@ SpaceOS 6(已停更) | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos7.html b/docs/spaceos7.html index d4e10c2..e75453f 100644 --- a/docs/spaceos7.html +++ b/docs/spaceos7.html @@ -6,14 +6,14 @@ SpaceOS 7(已停更) | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos8-download.html b/docs/spaceos8-download.html index dafefe4..fa88f9b 100644 --- a/docs/spaceos8-download.html +++ b/docs/spaceos8-download.html @@ -6,14 +6,14 @@ 下载 SpaceOS 8 | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos8.html b/docs/spaceos8.html index 8b03481..af7baa9 100644 --- a/docs/spaceos8.html +++ b/docs/spaceos8.html @@ -6,14 +6,14 @@ SpaceOS 8 | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos9-download.html b/docs/spaceos9-download.html index cd82948..d954d72 100644 --- a/docs/spaceos9-download.html +++ b/docs/spaceos9-download.html @@ -6,14 +6,14 @@ 下载 SpaceOS 9 | GoutouStdio - - + + - + - + + + + + + diff --git a/docs/spaceos9.html b/docs/spaceos9.html index 23cca39..ff00511 100644 --- a/docs/spaceos9.html +++ b/docs/spaceos9.html @@ -6,14 +6,14 @@ SpaceOS 9 | GoutouStdio - - + + - + - + + + + + + diff --git a/tools/checks/check_design_layer.py b/tools/checks/check_design_layer.py index dffd4e6..6884360 100644 --- a/tools/checks/check_design_layer.py +++ b/tools/checks/check_design_layer.py @@ -24,8 +24,18 @@ # Site-wide paper grain: a purely decorative pseudo-element carrying a noise background, no text in it, # and pointer-events: none, so it takes no part in interaction. 'body::after', + # Pointer-follow highlight on cards: two empty pseudo-elements (inner glow and lit border) that fade + # in on hover. They hold no text and take no pointer events; the card content never changes opacity. + '.spot::before', + '.spot::after', + '.spot:hover::before', + '.spot:hover::after', } +# opacity is allowed inside @keyframes: an animation only passes through a translucent state on its way +# in or out, and every animation is switched off under prefers-reduced-motion. A resting state that is +# dimmed with opacity is still caught by the rule below. + # Read a CSS file as UTF-8 text. def read(p): @@ -80,10 +90,26 @@ def check_token_refs(all_css, defined, label): return missing -# Flag opacity or filter applied to whole content blocks, whitelist aside. +# Remove every @keyframes block, braces balanced, so its frames are not read as selectors. +def strip_keyframes(css): + out, i = [], 0 + for m in re.finditer(r"@(?:-webkit-)?keyframes\b[^{]*\{", css): + if m.start() < i: + continue + out.append(css[i:m.start()]) + depth, j = 1, m.end() + while j < len(css) and depth: + depth += {"{": 1, "}": -1}.get(css[j], 0) + j += 1 + i = j + out.append(css[i:]) + return "".join(out) + + +# Flag opacity or filter applied to whole content blocks, whitelist and @keyframes aside. def check_no_opacity_on_content(css, name): bad = [] - body = strip_comments(css) + body = strip_keyframes(strip_comments(css)) for m in re.finditer(r'([^{}]+)\{([^{}]*)\}', body): raw_sel, decl = m.group(1).strip(), m.group(2) if not raw_sel or raw_sel.startswith('@'): diff --git a/tools/sync_docs_chrome.py b/tools/sync_docs_chrome.py index 447a31e..a54c504 100644 --- a/tools/sync_docs_chrome.py +++ b/tools/sync_docs_chrome.py @@ -119,6 +119,15 @@ BLOCKS = ("head", "chrome", "foot") +# Third-party animation libraries, loaded in this order before js/motion.js. Versions and licences +# are listed in docs/js/vendor/README.md. +VENDOR_SCRIPTS = [ + "js/vendor/gsap.min.js", + "js/vendor/ScrollTrigger.min.js", + "js/vendor/SplitText.min.js", + "js/vendor/lenis.min.js", +] + # Every page in docs/ and docs/ota/, as paths relative to docs/ with forward slashes. def page_paths(): @@ -261,6 +270,10 @@ def render_foot(page): " ", f' ', ] + # Animation stack, served from docs/js/vendor so the site never depends on a CDN. + # motion.js does nothing when any of these fail to load or the visitor asks for reduced motion. + for rel in VENDOR_SCRIPTS + ["js/motion.js"]: + out.append(f' ') return "\n".join(out) From bfc1c87f80ea3ae16caf8a1dea6bca73cfcd0835 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 01:44:58 +0000 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20=E6=89=8B=E6=9C=BA=E7=AB=AF?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=94=B9=E4=B8=BA=E5=A0=86=E5=8F=A0=E5=8D=A1?= =?UTF-8?q?=E7=89=87=EF=BC=8C=E7=89=B9=E6=80=A7=E5=88=97=E8=A1=A8=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=BC=96=E5=8F=B7=E5=8D=A1=E7=89=87=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表格在 48rem 以下每行变成一张卡片,行标题在上,值前面补列名(main.js 把表头写进 data-label), 不再把说明列挤成几个字宽,徽标也不再被裁掉 - .rows-wide 特性列表改为带 01/02 编号的卡片网格:桌面两列,手机一列 - 中文排版:text-autospace、text-spacing-trim、line-break: strict,段落 text-wrap: pretty 避免末行孤字 - 行内代码缩小一号并用 box-decoration-break: clone,折行时两段都保留完整圆角 - 配置要求页左侧图标标题与其他页的小节标题一样吸顶 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdLhesahBsuQSbGntwoQFN --- docs/css/style.css | 115 +++++++++++++++++++++++++++++++++++- docs/index.html | 4 +- docs/js/main.js | 15 +++++ docs/ota/index.html | 4 +- docs/ota/releases.html | 4 +- docs/pyspos.html | 4 +- docs/requirements.html | 4 +- docs/spaceos6.html | 4 +- docs/spaceos7.html | 4 +- docs/spaceos8-download.html | 4 +- docs/spaceos8.html | 4 +- docs/spaceos9-download.html | 4 +- docs/spaceos9.html | 4 +- 13 files changed, 149 insertions(+), 25 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index b2c0e90..b5444b8 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -40,8 +40,15 @@ body { -moz-osx-font-smoothing: grayscale; font-feature-settings: "cv11", "ss01"; text-rendering: optimizeLegibility; + /* 中西文之间自动留出间距、行首行尾标点挤压、避免标点出现在行首 */ + text-autospace: normal; + text-spacing-trim: normal; + line-break: strict; } +/* 段落最后一行不要只剩一两个字 */ +p, li, td, dd, figcaption { text-wrap: pretty; } + /* 全站颗粒质感:纯装饰的噪点层,不含文字,不接收指针事件 */ body::after { content: ""; @@ -486,6 +493,8 @@ html.motion:not(.motion-ready) .hero :is(h1, .lede, .actions, .hero-badge, .spec letter-spacing: -0.02em; } .split > div { min-width: 0; } +.split > .split-head { position: sticky; top: calc(var(--pg-header-h) + var(--pg-space-5)); } +.split-head h2 { font-size: var(--pg-text-xl); line-height: var(--pg-leading-tight); letter-spacing: -0.02em; } .stack > * + * { margin-top: var(--pg-space-4); } .stack > h3 { margin-top: var(--pg-space-7); } @@ -613,7 +622,49 @@ html.motion:not(.motion-ready) .hero :is(h1, .lede, .actions, .hero-badge, .spec .rows > li:hover .key { color: var(--pg-accent); } .rows .key a { color: inherit; } .rows p { color: var(--pg-fg-2); } -.rows-wide > li { grid-template-columns: 12rem minmax(0, 1fr); } +/* 特性列表:带编号的卡片网格,标题在上、说明在下 */ +.rows-wide { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--pg-space-4); + counter-reset: feat; +} +.rows-wide > li, +.rows-wide > li:last-child { + display: flex; + flex-direction: column; + gap: var(--pg-space-2); + padding: var(--pg-space-5); + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-glass); + border-radius: var(--pg-radius-lg); + box-shadow: inset 0 1px 0 var(--pg-line-hi); + counter-increment: feat; + transition: border-color var(--pg-dur-2) ease, translate var(--pg-dur-3) var(--pg-ease-out), + box-shadow var(--pg-dur-2) ease; +} +.rows-wide > li:hover { border-color: var(--pg-line-strong); translate: 0 -0.1875rem; box-shadow: var(--pg-shadow); } +/* 编号:01、02……,等宽字体、品牌渐变 */ +.rows-wide > li::before, +.rows-wide > li:hover::before { + content: counter(feat, decimal-leading-zero); + position: static; + width: auto; + height: auto; + font-family: var(--pg-font-mono); + font-size: var(--pg-text-xs); + font-weight: 700; + letter-spacing: var(--pg-tracking-label); + line-height: 1; + background: var(--pg-grad-brand); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; + transform: none; +} +.rows-wide .key { font-size: var(--pg-text-lg); } +.rows-wide p { font-size: var(--pg-text-sm); line-height: 1.8; } .rows p + p { margin-top: var(--pg-space-2); } /* ---------- 代码 ---------- */ @@ -622,11 +673,15 @@ code, kbd, pre { font-size: 0.875em; } code { - padding: 0.1em 0.4em; + padding: 0.08em 0.38em; + font-size: 0.84em; background: var(--pg-accent-soft); border: var(--pg-hairline) solid var(--pg-line-glass); border-radius: var(--pg-radius-sm); overflow-wrap: anywhere; + /* 折行时每一段都保留完整的圆角和内边距 */ + -webkit-box-decoration-break: clone; + box-decoration-break: clone; } pre { position: relative; @@ -1235,7 +1290,10 @@ html.vt-theme::view-transition-new(root) { animation: none; mix-blend-mode: norm .container { padding-inline: var(--pg-space-4); } .hero { padding-block: var(--pg-space-8) var(--pg-space-7); } .hero-split, .split, .release { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-5); } - .split > h2 { position: static; } + .split > h2, .split > .split-head { position: static; } + .rows-wide { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-3); } + .rows-wide > li, .rows-wide > li:last-child { padding: var(--pg-space-4) var(--pg-space-5); } + .lede { font-size: 1.0625rem; } .section { padding-block: var(--pg-space-8); } .rows > li, .rows-wide > li { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-1); } .points { grid-template-columns: minmax(0, 1fr); } @@ -1273,6 +1331,57 @@ html.vt-theme::view-transition-new(root) { animation: none; mix-blend-mode: norm .site-nav .menu.is-current > summary::after { left: var(--pg-space-3); margin-left: 0; } } +/* 手机上表格改成一行一张卡片:行标题在上,每个值前面补上列名(由 main.js 写进 data-label)。 + 这些表每一行彼此独立,不需要跨行横向比较,所以堆叠比横向滚动更好读。 */ +@media (max-width: 48rem) { + .table-wrap { + padding: 0; + overflow: visible; + background: none; + border: 0; + border-radius: 0; + box-shadow: none; + } + .table-wrap :is(table, tbody, tr, th, td) { display: block; width: auto; } + .table-wrap thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } + .table-wrap tr { + padding: var(--pg-space-4) var(--pg-space-5); + background: var(--pg-bg-card); + border: var(--pg-hairline) solid var(--pg-line-glass); + border-radius: var(--pg-radius-lg); + box-shadow: inset 0 1px 0 var(--pg-line-hi); + } + .table-wrap tr + tr { margin-top: var(--pg-space-3); } + .table-wrap tbody tr:hover { background: var(--pg-bg-card); } + .table-wrap :is(th, td), + .table-wrap tbody tr:last-child > * { padding: 0; border: 0; text-align: left; white-space: normal; } + .table-wrap tbody th { font-size: var(--pg-text-md); line-height: var(--pg-leading-tight); } + .table-wrap tbody th + td, + .table-wrap td + td { margin-top: var(--pg-space-2); } + .table-wrap td:empty { display: none; } + .table-wrap td[data-label]::before { + content: attr(data-label); + display: block; + margin-bottom: 0.125rem; + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); + font-weight: 600; + letter-spacing: var(--pg-tracking-label); + text-transform: uppercase; + color: var(--pg-fg-3); + } + .table-wrap .badge { white-space: normal; } + .table-wrap td.num { text-align: left; } + .num .bar { margin-left: 0; } +} + /* 用户要求减少动态效果时,关掉所有装饰性动画与过渡 */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { diff --git a/docs/index.html b/docs/index.html index 2b32ed3..87bc0b0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/js/main.js b/docs/js/main.js index d9013cb..bd3d98a 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -132,10 +132,25 @@ }); } + // 手机上表格会堆叠成卡片,每个单元格前要显示列名;这里把表头文字抄到 data-label。 + function initTableLabels() { + document.querySelectorAll("main table").forEach(function (table) { + var heads = table.querySelectorAll("thead th"); + if (!heads.length) return; + table.querySelectorAll("tbody tr").forEach(function (tr) { + Array.prototype.forEach.call(tr.children, function (cell, i) { + var label = heads[i] && heads[i].textContent.trim(); + if (cell.tagName === "TD" && label) cell.setAttribute("data-label", label); + }); + }); + }); + } + function init() { initTheme(); initMenu(); initCopy(); + initTableLabels(); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); diff --git a/docs/ota/index.html b/docs/ota/index.html index dbfe393..986860b 100644 --- a/docs/ota/index.html +++ b/docs/ota/index.html @@ -7,7 +7,7 @@ PySpOS 更新包 | GoutouStdio - + @@ -224,7 +224,7 @@ - + diff --git a/docs/ota/releases.html b/docs/ota/releases.html index b2ab740..7dba6a1 100644 --- a/docs/ota/releases.html +++ b/docs/ota/releases.html @@ -7,7 +7,7 @@ PySpOS 版本历史 | GoutouStdio - + @@ -154,7 +154,7 @@ - + diff --git a/docs/pyspos.html b/docs/pyspos.html index 8b923f6..22b8a50 100644 --- a/docs/pyspos.html +++ b/docs/pyspos.html @@ -7,7 +7,7 @@ PySpOS | GoutouStdio - + @@ -289,7 +289,7 @@ - + diff --git a/docs/requirements.html b/docs/requirements.html index 4500231..f806bc5 100644 --- a/docs/requirements.html +++ b/docs/requirements.html @@ -7,7 +7,7 @@ 配置要求 | GoutouStdio - + @@ -223,7 +223,7 @@ - + diff --git a/docs/spaceos6.html b/docs/spaceos6.html index 8b84347..813e2fe 100644 --- a/docs/spaceos6.html +++ b/docs/spaceos6.html @@ -7,7 +7,7 @@ SpaceOS 6(已停更) | GoutouStdio - + @@ -189,7 +189,7 @@ - + diff --git a/docs/spaceos7.html b/docs/spaceos7.html index e75453f..c784bb5 100644 --- a/docs/spaceos7.html +++ b/docs/spaceos7.html @@ -7,7 +7,7 @@ SpaceOS 7(已停更) | GoutouStdio - + @@ -198,7 +198,7 @@ - + diff --git a/docs/spaceos8-download.html b/docs/spaceos8-download.html index fa88f9b..a7e9047 100644 --- a/docs/spaceos8-download.html +++ b/docs/spaceos8-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 8 | GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/spaceos8.html b/docs/spaceos8.html index af7baa9..ebebee6 100644 --- a/docs/spaceos8.html +++ b/docs/spaceos8.html @@ -7,7 +7,7 @@ SpaceOS 8 | GoutouStdio - + @@ -343,7 +343,7 @@ - + diff --git a/docs/spaceos9-download.html b/docs/spaceos9-download.html index d954d72..0e6b01f 100644 --- a/docs/spaceos9-download.html +++ b/docs/spaceos9-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 9 | GoutouStdio - + @@ -232,7 +232,7 @@ - + diff --git a/docs/spaceos9.html b/docs/spaceos9.html index ff00511..b58e6bd 100644 --- a/docs/spaceos9.html +++ b/docs/spaceos9.html @@ -7,7 +7,7 @@ SpaceOS 9 | GoutouStdio - + @@ -245,7 +245,7 @@ - + From 96c65f4948e9448f44efbdffb8fcbc67534024b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 02:02:08 +0000 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E9=95=BF=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=8A=98=E5=8F=A0=E3=80=81=E6=A8=AA=E6=BB=91?= =?UTF-8?q?=E5=92=8C=E6=9C=AC=E9=A1=B5=E7=9B=AE=E5=BD=95=EF=BC=8C=E4=BB=8B?= =?UTF-8?q?=E7=BB=8D=E9=A1=B5=E5=A4=A7=E5=B9=85=E7=BC=A9=E7=9F=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 小节多的页面在首屏下方加吸顶的本页目录,滚动时高亮当前小节,页眉收起时贴顶 - 特性卡片:电脑上先显示 4 张,其余用「展开其余 N 项」按钮展开; 手机上改为横向滑动的卡片带,下方有进度条和「当前 / 总数」 - 超过 5 行的表格先显示 4 行;配置要求页的表格标了 data-nofold,不折叠 - 手机表格卡片:状态徽标和数字列挪到标题右边,说明列不再重复显示列名 - 窄屏代码块的复制按钮移到代码上方,不再压住长命令 - 全部是渐进增强:没有脚本时内容全部平铺 - 手机上 PySpOS 页从约 10.7 屏缩到 7.6 屏,SpaceOS 8 页从 16.3 屏缩到 11.7 屏 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdLhesahBsuQSbGntwoQFN --- docs/css/style.css | 164 +++++++++++++++++++++++++++++++++++- docs/index.html | 6 +- docs/js/main.js | 144 ++++++++++++++++++++++++++++++- docs/js/motion.js | 10 +++ docs/ota/index.html | 6 +- docs/ota/releases.html | 6 +- docs/pyspos.html | 6 +- docs/requirements.html | 12 +-- docs/spaceos6.html | 6 +- docs/spaceos7.html | 6 +- docs/spaceos8-download.html | 6 +- docs/spaceos8.html | 6 +- docs/spaceos9-download.html | 6 +- docs/spaceos9.html | 6 +- 14 files changed, 351 insertions(+), 39 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index b5444b8..461676b 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1331,6 +1331,142 @@ html.vt-theme::view-transition-new(root) { animation: none; mix-blend-mode: norm .site-nav .menu.is-current > summary::after { left: var(--pg-space-3); margin-left: 0; } } + +/* ---------- 长内容:折叠按钮、横滑卡片、本页目录 ---------- */ +/* 折叠:表格只露前 4 行、特性卡片在电脑上只露前 4 张,底部渐隐提示下面还有 */ +table.is-collapsed tbody > tr:nth-child(n+5) { display: none; } +.rows-wide.is-collapsed > li:nth-child(n+5) { display: none; } +.table-wrap:has(table.is-collapsed), +.rows-wide.is-collapsed { + -webkit-mask-image: linear-gradient(180deg, black 65%, transparent); + mask-image: linear-gradient(180deg, black 65%, transparent); +} +.fold-btn { + display: flex; + align-items: center; + justify-content: center; + gap: var(--pg-space-2); + width: 100%; + margin-top: var(--pg-space-3); + padding: var(--pg-space-3) var(--pg-space-4); + font: inherit; + font-size: var(--pg-text-sm); + font-weight: 600; + color: var(--pg-fg-1); + background: var(--pg-bg-card); + border: var(--pg-hairline) dashed var(--pg-line-strong); + border-radius: var(--pg-radius-lg); + cursor: pointer; + transition: border-color var(--pg-dur-2) ease, background-color var(--pg-dur-2) ease; +} +.fold-btn:hover { border-color: var(--pg-accent); border-style: solid; background: var(--pg-accent-soft); } +.fold-btn .icon { transition: transform var(--pg-dur-2) var(--pg-ease-spring); } +.fold-btn[aria-expanded="true"] .icon { transform: rotate(180deg); } +.swipe-meta { display: none; } +/* 列表直接放在两栏 .split 里时,按钮跟着列表待在右栏,并收紧和列表之间的行距 */ +@media (min-width: 56.01rem) { + .split > .fold-btn { grid-column: 2; margin-top: calc(var(--pg-space-7) * -1 + var(--pg-space-3)); } +} + +/* 本页目录:首屏下方吸顶;页眉收起时贴到最顶上 */ +.page-toc { + position: sticky; + top: var(--pg-header-h); + z-index: 40; + background: var(--pg-bg-glass); + border-block: var(--pg-hairline) solid var(--pg-line-glass); + -webkit-backdrop-filter: saturate(1.6) blur(var(--pg-blur)); + backdrop-filter: saturate(1.6) blur(var(--pg-blur)); + transition: top var(--pg-dur-3) var(--pg-ease-out); +} +.site-header.is-hidden ~ main .page-toc { top: 0; } +html.has-toc { scroll-padding-top: calc(var(--pg-header-h) + 3.5rem); } +.page-toc ul { + display: flex; + gap: var(--pg-space-1); + max-width: var(--pg-container); + margin: 0 auto; + padding: var(--pg-space-2) var(--pg-space-5); + overflow-x: auto; + list-style: none; + scrollbar-width: none; +} +.page-toc ul::-webkit-scrollbar { display: none; } +.page-toc li { flex: none; margin: 0; } +.page-toc a { + display: block; + padding: var(--pg-space-1) var(--pg-space-3); + font-size: var(--pg-text-xs); + font-weight: 500; + white-space: nowrap; + color: var(--pg-fg-2); + text-decoration: none; + border: var(--pg-hairline) solid transparent; + border-radius: var(--pg-radius-pill); + transition: color var(--pg-dur-1) ease, background-color var(--pg-dur-2) ease, border-color var(--pg-dur-2) ease; +} +.page-toc a:hover { color: var(--pg-fg-1); background: var(--pg-accent-soft); } +.page-toc a[aria-current] { + color: var(--pg-fg-1); + font-weight: 650; + background: var(--pg-accent-soft); + border-color: var(--pg-line-strong); +} +.page-toc + .section::before { display: none; } + +/* 手机:特性卡片改成横向滑动,露出下一张的边缘提示还能滑 */ +@media (max-width: 48rem) { + .rows-wide, + .rows-wide.is-collapsed { + grid-template-columns: none; + grid-auto-flow: column; + grid-auto-columns: 84%; + margin-inline: calc(var(--pg-space-4) * -1); + padding: 0 var(--pg-space-4) var(--pg-space-1); + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; + scroll-padding-inline: var(--pg-space-4); + scrollbar-width: none; + -webkit-mask-image: none; + mask-image: none; + } + .rows-wide::-webkit-scrollbar { display: none; } + .rows-wide > li { scroll-snap-align: start; align-self: start; } + .split > .swipe-meta { margin-top: calc(var(--pg-space-5) * -1 + var(--pg-space-3)); } + .rows-wide.is-collapsed > li:nth-child(n+5) { display: flex; } + .fold-btn.fold-cards { display: none; } + .swipe-meta { + display: flex; + align-items: center; + gap: var(--pg-space-3); + margin-top: var(--pg-space-3); + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); + color: var(--pg-fg-3); + } + .swipe-meta[hidden] { display: none; } + .swipe-track { + position: relative; + flex: 1; + height: 0.1875rem; + overflow: hidden; + background: var(--pg-line-2); + border-radius: var(--pg-radius-pill); + } + .swipe-track i { + position: absolute; + top: 0; + bottom: 0; + left: 0; + background: var(--pg-grad-brand); + border-radius: inherit; + } + .page-toc ul { padding-inline: var(--pg-space-4); } + /* 窄屏代码块:复制按钮放到代码上方一条,不再压住长命令 */ + pre:has(.copy-btn) { padding-top: calc(var(--pg-space-7) + var(--pg-space-1)); padding-right: var(--pg-space-5); } +} + /* 手机上表格改成一行一张卡片:行标题在上,每个值前面补上列名(由 main.js 写进 data-label)。 这些表每一行彼此独立,不需要跨行横向比较,所以堆叠比横向滚动更好读。 */ @media (max-width: 48rem) { @@ -1378,8 +1514,34 @@ html.vt-theme::view-transition-new(root) { animation: none; mix-blend-mode: norm color: var(--pg-fg-3); } .table-wrap .badge { white-space: normal; } + /* 只有一个状态徽标的格子不再单占一行:挪到卡片标题右边,也不再显示列名 */ + .table-wrap tbody tr:has(> th) { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + column-gap: var(--pg-space-3); + align-items: start; + } + .table-wrap tbody tr:has(> th) > * { grid-column: 1 / -1; } + .table-wrap tbody tr:has(> th) > th { grid-column: 1; grid-row: 1; } + .table-wrap tbody tr > td:has(> .badge:only-child) { + grid-column: 2; + grid-row: 1; + margin-top: 0; + } + .table-wrap td:has(> .badge:only-child)::before { display: none; } + /* 数字列(行数等)同样放到标题右边,条形图缩短后跟在数字上方 */ + .table-wrap tbody tr > td.num { + grid-column: 2; + grid-row: 1; + margin-top: 0; + text-align: right; + } + .table-wrap td.num::before { display: none; } + .table-wrap td.num .bar { width: 4.5rem; margin-left: auto; } + .table-wrap tbody tr > th + td + td.num, + .table-wrap tbody tr > th + td:has(+ td.num) { margin-top: var(--pg-space-2); } + .table-wrap tbody tr > th + td:has(> .badge:only-child) + td { margin-top: var(--pg-space-2); } .table-wrap td.num { text-align: left; } - .num .bar { margin-left: 0; } } /* 用户要求减少动态效果时,关掉所有装饰性动画与过渡 */ diff --git a/docs/index.html b/docs/index.html index 87bc0b0..7506733 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ GoutouStdio - + @@ -239,12 +239,12 @@ - + - + diff --git a/docs/js/main.js b/docs/js/main.js index bd3d98a..bcfb01d 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -137,20 +137,160 @@ document.querySelectorAll("main table").forEach(function (table) { var heads = table.querySelectorAll("thead th"); if (!heads.length) return; - table.querySelectorAll("tbody tr").forEach(function (tr) { + var rows = table.querySelectorAll("tbody tr"); + // 文字最长的那一列就是说明本身,不需要再标列名,免得每张卡片都多一行「说明」「作用」 + var totals = []; + rows.forEach(function (tr) { + Array.prototype.forEach.call(tr.children, function (cell, i) { + totals[i] = (totals[i] || 0) + cell.textContent.trim().length; + }); + }); + var main = totals.indexOf(Math.max.apply(null, totals)); + rows.forEach(function (tr) { Array.prototype.forEach.call(tr.children, function (cell, i) { var label = heads[i] && heads[i].textContent.trim(); - if (cell.tagName === "TD" && label) cell.setAttribute("data-label", label); + if (cell.tagName === "TD" && label && i !== main) cell.setAttribute("data-label", label); }); }); }); } + + /* ---------- 长内容:折叠、横滑、本页目录 ---------- + 三项都只在有脚本时生效;没有脚本时内容全部平铺,一个字都不会被藏起来。 */ + + var FOLD_ROWS = 4; // 表格超过 FOLD_ROWS + 1 行时,先显示 FOLD_ROWS 行 + var FOLD_CARDS = 4; // 特性卡片超过 FOLD_CARDS 张时,电脑上先显示 FOLD_CARDS 张 + var uid = 0; + + function makeFold(list, items, keep, extraClass) { + if (items.length <= keep + 1) return; + if (!list.id) list.id = "fold-" + (++uid); + var rest = items.length - keep; + var btn = document.createElement("button"); + btn.type = "button"; + btn.className = "fold-btn" + (extraClass ? " " + extraClass : ""); + btn.setAttribute("aria-controls", list.id); + function set(open) { + list.classList.toggle("is-collapsed", !open); + btn.setAttribute("aria-expanded", String(open)); + btn.innerHTML = (open ? "收起" : "展开其余 " + rest + " 项") + + ''; + } + set(false); + btn.addEventListener("click", function () { + var open = list.classList.contains("is-collapsed"); + set(open); + if (open) { + if (window.pgMotion) window.pgMotion.show(items.slice(keep)); + } else { + // 收起时把按钮带回视野,免得停在一大片空白里 + var top = list.getBoundingClientRect().top; + if (top < 0) list.scrollIntoView({ block: "start" }); + } + if (window.ScrollTrigger) window.ScrollTrigger.refresh(); + }); + var anchor = list.closest(".table-wrap") || list; + anchor.insertAdjacentElement("afterend", btn); + } + + function initFold() { + document.querySelectorAll("main .table-wrap:not([data-nofold]) table").forEach(function (table) { + var rows = Array.prototype.slice.call(table.querySelectorAll("tbody > tr")); + makeFold(table, rows, FOLD_ROWS); + }); + document.querySelectorAll("main .rows-wide:not([data-nofold])").forEach(function (list) { + var items = Array.prototype.slice.call(list.children); + // 手机上这类列表改为横滑卡片,不需要折叠按钮(CSS 在窄屏隐藏 .fold-cards) + makeFold(list, items, FOLD_CARDS, "fold-cards"); + }); + } + + // 手机上特性卡片横向滑动:下方一条进度条和「当前 / 总数」 + function initCarousel() { + document.querySelectorAll("main .rows-wide").forEach(function (list) { + var total = list.children.length; + if (total < 2) return; + var meta = document.createElement("div"); + meta.className = "swipe-meta"; + meta.setAttribute("aria-hidden", "true"); + meta.innerHTML = ''; + list.insertAdjacentElement("afterend", meta); + var thumb = meta.querySelector("i"); + var count = meta.querySelector(".swipe-count"); + function update() { + var max = list.scrollWidth - list.clientWidth; + if (max <= 1) { meta.hidden = true; return; } + meta.hidden = false; + var ratio = list.clientWidth / list.scrollWidth; + var p = list.scrollLeft / max; + thumb.style.width = (ratio * 100) + "%"; + thumb.style.transform = "translateX(" + (p * (1 / ratio - 1) * 100) + "%)"; + var card = list.children[0].getBoundingClientRect().width || 1; + var idx = Math.min(total, Math.round(list.scrollLeft / card) + 1); + count.textContent = "左右滑动 · " + idx + " / " + total; + } + list.addEventListener("scroll", update, { passive: true }); + window.addEventListener("resize", update); + update(); + }); + } + + // 本页目录:小节多的页面,在首屏下方放一条吸顶的目录,滚动时高亮当前小节 + function initToc() { + var main = document.querySelector("main"); + if (!main) return; + var sections = Array.prototype.slice.call(main.querySelectorAll(":scope > section.section[id]")).filter(function (s) { + return s.querySelector("h2"); + }); + if (sections.length < 4) return; + var nav = document.createElement("nav"); + nav.className = "page-toc"; + nav.setAttribute("aria-label", "本页目录"); + var ul = document.createElement("ul"); + var links = sections.map(function (s) { + var li = document.createElement("li"); + var a = document.createElement("a"); + a.href = "#" + s.id; + a.textContent = s.querySelector("h2").textContent.trim(); + li.appendChild(a); + ul.appendChild(li); + return a; + }); + nav.appendChild(ul); + var hero = main.querySelector(":scope > .hero"); + if (hero) hero.insertAdjacentElement("afterend", nav); + else main.prepend(nav); + document.documentElement.classList.add("has-toc"); + + if (!("IntersectionObserver" in window)) return; + var current = null; + function activate(a) { + if (current === a) return; + if (current) current.removeAttribute("aria-current"); + current = a; + if (!a) return; + a.setAttribute("aria-current", "true"); + // 只横向滚动目录本身,不带动页面 + var left = a.offsetLeft - (ul.clientWidth - a.offsetWidth) / 2; + ul.scrollTo({ left: Math.max(0, left), behavior: "smooth" }); + } + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { + if (e.isIntersecting) activate(links[sections.indexOf(e.target)]); + }); + }, { rootMargin: "-40% 0px -55% 0px" }); + sections.forEach(function (s) { io.observe(s); }); + } + function init() { initTheme(); initMenu(); initCopy(); initTableLabels(); + initToc(); + initFold(); + initCarousel(); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); diff --git a/docs/js/motion.js b/docs/js/motion.js index 6961ee5..1a9ccf5 100644 --- a/docs/js/motion.js +++ b/docs/js/motion.js @@ -449,6 +449,16 @@ reveal($$(".footer-grid > *", footer), { y: 24 }, { stagger: 0.07, start: "top 95%" }); } + // main.js 展开折叠内容后调用:新露出的条目依次浮现 + window.pgMotion = { + show: function (items) { + gsap.fromTo(items, { y: 24, autoAlpha: 0 }, { + y: 0, autoAlpha: 1, duration: 0.7, stagger: 0.05, ease: "expo.out", overwrite: true, + clearProps: "transform,opacity,visibility" + }); + } + }; + function init() { initLenis(); initChrome(); diff --git a/docs/ota/index.html b/docs/ota/index.html index 986860b..ad9e692 100644 --- a/docs/ota/index.html +++ b/docs/ota/index.html @@ -7,7 +7,7 @@ PySpOS 更新包 | GoutouStdio - + @@ -224,12 +224,12 @@ - + - + + - + + - + diff --git a/docs/requirements.html b/docs/requirements.html index f806bc5..549df5e 100644 --- a/docs/requirements.html +++ b/docs/requirements.html @@ -7,7 +7,7 @@ 配置要求 | GoutouStdio - + @@ -88,7 +88,7 @@

配置要求

PySpOS 3.2.0

纯用户态,不直接操作硬件,能跑 Python 的机器就能跑。

-
+
@@ -110,7 +110,7 @@

配置要求

SpaceOS 9 公开预览

目前只在 QEMU 上验证过。

-
+
Python3.10 或更高。CI 覆盖 3.10、3.12、3.14。
@@ -139,7 +139,7 @@

配置要求

表里“已验证”是实际跑过的组合,“默认关闭”的需要自己在 make menuconfig 里打开。

-
+
虚拟机x86_64 QEMU,推荐 Q35 机型和 VirtIO GPU 2D。
@@ -223,12 +223,12 @@ - + - + diff --git a/docs/spaceos6.html b/docs/spaceos6.html index 813e2fe..c5ea896 100644 --- a/docs/spaceos6.html +++ b/docs/spaceos6.html @@ -7,7 +7,7 @@ SpaceOS 6(已停更) | GoutouStdio - + @@ -189,12 +189,12 @@ - + - + diff --git a/docs/spaceos7.html b/docs/spaceos7.html index c784bb5..b56f7cc 100644 --- a/docs/spaceos7.html +++ b/docs/spaceos7.html @@ -7,7 +7,7 @@ SpaceOS 7(已停更) | GoutouStdio - + @@ -198,12 +198,12 @@ - + - + diff --git a/docs/spaceos8-download.html b/docs/spaceos8-download.html index a7e9047..0284059 100644 --- a/docs/spaceos8-download.html +++ b/docs/spaceos8-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 8 | GoutouStdio - + @@ -239,12 +239,12 @@ - + - + diff --git a/docs/spaceos8.html b/docs/spaceos8.html index ebebee6..a9e16a2 100644 --- a/docs/spaceos8.html +++ b/docs/spaceos8.html @@ -7,7 +7,7 @@ SpaceOS 8 | GoutouStdio - + @@ -343,12 +343,12 @@ - + - + diff --git a/docs/spaceos9-download.html b/docs/spaceos9-download.html index 0e6b01f..cc763ae 100644 --- a/docs/spaceos9-download.html +++ b/docs/spaceos9-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 9 | GoutouStdio - + @@ -232,12 +232,12 @@ - + - + diff --git a/docs/spaceos9.html b/docs/spaceos9.html index b58e6bd..61436be 100644 --- a/docs/spaceos9.html +++ b/docs/spaceos9.html @@ -7,7 +7,7 @@ SpaceOS 9 | GoutouStdio - + @@ -245,12 +245,12 @@ - + - + From e4429d4723c8c3bcf14b41dd08db4bae2cc465a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 02:26:50 +0000 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=E4=BB=8B=E7=BB=8D=E9=A1=B5?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20Markdown=20=E5=BC=8F=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=8E=92=E7=89=88=E5=B9=B6=E7=B2=BE=E7=AE=80=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PySpOS、SpaceOS 9 / 8 / 7 / 6 五个介绍页:首屏保留,以下改成单栏文章, 左侧吸顶目录(手机上是顶部目录条),滚动时高亮当前小节 - 排版仿渲染后的 Markdown:带 # / ## 的小节标题和下边线、短横线列表、 等宽编号的有序列表、GitHub 风格提示块、带边框和隔行底色的表格 - 去掉这些页上的要点卡片、流程图、分层图和特性卡片网格,改为列表 - 文案按短句、一句一事重写,正文字数约减半(SpaceOS 8 3348→1637 字,PySpOS 2631→1433 字) - 文档页里的表格不再折叠;main.js 的目录高亮同时支持静态目录和自动生成的目录 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdLhesahBsuQSbGntwoQFN --- docs/css/style.css | 161 ++++++++++++++++++++++ docs/index.html | 4 +- docs/js/main.js | 19 ++- docs/ota/index.html | 4 +- docs/ota/releases.html | 4 +- docs/pyspos.html | 186 +++++++++----------------- docs/requirements.html | 4 +- docs/spaceos6.html | 88 ++++++------ docs/spaceos7.html | 96 ++++++-------- docs/spaceos8-download.html | 4 +- docs/spaceos8.html | 257 +++++++++++++----------------------- docs/spaceos9-download.html | 4 +- docs/spaceos9.html | 146 ++++++++------------ 13 files changed, 488 insertions(+), 489 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index 461676b..79bb35b 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1544,6 +1544,167 @@ html.has-toc { scroll-padding-top: calc(var(--pg-header-h) + 3.5rem); } .table-wrap td.num { text-align: left; } } +/* ---------- 文档式正文(介绍页):排版像渲染后的 Markdown ---------- + 左边吸顶目录,右边单栏文章。只用标题、段落、列表、表格和提示块,不用卡片网格。 */ +.doc-section { padding-block: var(--pg-space-8) var(--pg-space-9); } +.doc { + display: grid; + grid-template-columns: 12.5rem minmax(0, 46rem); + gap: var(--pg-space-8); + justify-content: center; + align-items: start; +} +.doc-toc-title { display: none; } + +.prose { + min-width: 0; + font-size: 1.03125rem; + line-height: 1.85; + color: var(--pg-fg-2); +} +.prose > * + * { margin-top: var(--pg-space-4); } +.prose > :first-child { margin-top: 0; } +.prose h2 { + margin-top: var(--pg-space-8); + padding-bottom: var(--pg-space-3); + font-size: var(--pg-text-xl); + line-height: var(--pg-leading-tight); + letter-spacing: -0.02em; + border-bottom: var(--pg-hairline) solid var(--pg-line-2); +} +.prose h3 { + margin-top: var(--pg-space-6); + font-size: var(--pg-text-lg); + letter-spacing: -0.01em; +} +/* 标题前的 # / ## 只是装饰,读屏时忽略(content 的 / "" 是替代文本) */ +.prose h2::before, +.prose h3::before { + content: "#"; + content: "#" / ""; + margin-right: 0.45em; + font-family: var(--pg-font-mono); + font-weight: 500; + color: var(--pg-brand); +} +.prose h3::before { content: "##"; content: "##" / ""; color: var(--pg-fg-3); } +.prose h2 + *, +.prose h3 + * { margin-top: var(--pg-space-3); } +.prose strong { font-weight: 650; color: var(--pg-fg-1); } +.prose a { text-decoration-color: var(--pg-line-strong); } +.prose a:hover { text-decoration-color: currentColor; } + +.prose ul, +.prose ol { padding-left: 1.5rem; list-style: none; } +.prose li { position: relative; } +.prose li + li { margin-top: var(--pg-space-2); } +.prose ul > li::before { + content: "-"; + content: "-" / ""; + position: absolute; + left: -1.25rem; + font-family: var(--pg-font-mono); + font-weight: 700; + color: var(--pg-brand); +} +.prose ol { counter-reset: md; } +.prose ol > li { counter-increment: md; } +.prose ol > li::before { + content: counter(md) "."; + position: absolute; + left: -1.5rem; + font-family: var(--pg-font-mono); + font-size: var(--pg-text-sm); + font-weight: 700; + color: var(--pg-brand); +} + +/* 提示块:GitHub 风格,左侧一条色条 */ +.prose .notice { + padding: var(--pg-space-3) var(--pg-space-4); + background: var(--pg-accent-soft); + border: 0; + border-left: 0.25rem solid var(--pg-brand); + border-radius: var(--pg-radius); + box-shadow: none; +} +.prose .notice-warn { background: var(--pg-warn-bg); border-left-color: var(--pg-warn-fg); } +.prose .notice p { font-size: var(--pg-text-sm); } + +/* 表格:Markdown 表格的样子,带边框和隔行底色(手机上仍是堆叠卡片) */ +@media (min-width: 48.01rem) { + .prose .table-wrap { + padding: 0; + background: none; + border: 0; + border-radius: 0; + box-shadow: none; + } + .prose table { font-size: var(--pg-text-sm); } + .prose :is(th, td), + .prose tbody tr:last-child > * { + padding: var(--pg-space-2) var(--pg-space-4); + border: var(--pg-hairline) solid var(--pg-line-2); + } + .prose thead th { color: var(--pg-fg-1); background: var(--pg-bg-sunken); } + .prose tbody tr:nth-child(even) { background: var(--pg-bg-sunken); } + .prose tbody tr:hover { background: var(--pg-accent-soft); } +} + +/* 电脑:目录是左侧的竖排列表,当前小节左边亮一条品牌色 */ +@media (min-width: 62.01rem) { + .doc > .doc-toc { + position: sticky; + top: calc(var(--pg-header-h) + var(--pg-space-5)); + background: none; + border: 0; + -webkit-backdrop-filter: none; + backdrop-filter: none; + } + .site-header.is-hidden ~ main .doc > .doc-toc { top: var(--pg-space-5); } + .doc-toc-title { + display: block; + margin-bottom: var(--pg-space-3); + font-family: var(--pg-font-mono); + font-size: var(--pg-text-2xs); + letter-spacing: var(--pg-tracking-label); + text-transform: uppercase; + color: var(--pg-fg-3); + } + .doc-toc ul { + flex-direction: column; + gap: 0; + padding: 0; + overflow: visible; + border-left: var(--pg-hairline) solid var(--pg-line-2); + } + .doc-toc a, + .doc-toc a:hover, + .doc-toc a[aria-current] { + margin-left: -1px; + padding: 0.375rem 0 0.375rem var(--pg-space-4); + font-size: var(--pg-text-sm); + white-space: normal; + background: none; + border: 0; + border-left: var(--pg-stroke) solid transparent; + border-radius: 0; + } + .doc-toc a:hover { color: var(--pg-fg-1); border-left-color: var(--pg-line-strong); } + .doc-toc a[aria-current] { color: var(--pg-fg-1); border-left-color: var(--pg-brand); } +} +/* 平板和手机:单栏,目录变成顶部横向的目录条 */ +@media (max-width: 62rem) { + .doc { grid-template-columns: minmax(0, 1fr); gap: var(--pg-space-6); } + .doc > .doc-toc { margin-inline: calc(var(--pg-space-5) * -1); } +} +@media (max-width: 56rem) { + .doc > .doc-toc { margin-inline: calc(var(--pg-space-4) * -1); } + .doc-section { padding-block: var(--pg-space-6) var(--pg-space-8); } + .prose { font-size: var(--pg-text-md); } + .prose h2 { margin-top: var(--pg-space-7); } +} + /* 用户要求减少动态效果时,关掉所有装饰性动画与过渡 */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { diff --git a/docs/index.html b/docs/index.html index 7506733..3434fb0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/js/main.js b/docs/js/main.js index bcfb01d..c44d911 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -196,6 +196,7 @@ function initFold() { document.querySelectorAll("main .table-wrap:not([data-nofold]) table").forEach(function (table) { + if (table.closest(".prose")) return; // 文档式正文已经精简过,表格全部展开 var rows = Array.prototype.slice.call(table.querySelectorAll("tbody > tr")); makeFold(table, rows, FOLD_ROWS); }); @@ -240,6 +241,15 @@ function initToc() { var main = document.querySelector("main"); if (!main) return; + // 文档式页面自带静态目录(没有脚本也能用),这里只负责高亮当前小节 + var docToc = main.querySelector(".doc-toc"); + if (docToc) { + var docLinks = Array.prototype.slice.call(docToc.querySelectorAll("a[href^='#']")); + var targets = docLinks.map(function (a) { return document.getElementById(a.getAttribute("href").slice(1)); }); + document.documentElement.classList.add("has-toc"); + spy(docToc.querySelector("ul"), docLinks, targets); + return; + } var sections = Array.prototype.slice.call(main.querySelectorAll(":scope > section.section[id]")).filter(function (s) { return s.querySelector("h2"); }); @@ -263,6 +273,11 @@ else main.prepend(nav); document.documentElement.classList.add("has-toc"); + spy(ul, links, sections); + } + + // 滚动时高亮当前小节:目标越过视口上方 40% 的位置就算进入 + function spy(ul, links, targets) { if (!("IntersectionObserver" in window)) return; var current = null; function activate(a) { @@ -277,10 +292,10 @@ } var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { - if (e.isIntersecting) activate(links[sections.indexOf(e.target)]); + if (e.isIntersecting) activate(links[targets.indexOf(e.target)]); }); }, { rootMargin: "-40% 0px -55% 0px" }); - sections.forEach(function (s) { io.observe(s); }); + targets.forEach(function (t) { if (t) io.observe(t); }); } function init() { diff --git a/docs/ota/index.html b/docs/ota/index.html index ad9e692..30ab09e 100644 --- a/docs/ota/index.html +++ b/docs/ota/index.html @@ -7,7 +7,7 @@ PySpOS 更新包 | GoutouStdio - + @@ -224,7 +224,7 @@ - + diff --git a/docs/ota/releases.html b/docs/ota/releases.html index a4e896b..ef4169a 100644 --- a/docs/ota/releases.html +++ b/docs/ota/releases.html @@ -7,7 +7,7 @@ PySpOS 版本历史 | GoutouStdio - + @@ -154,7 +154,7 @@ - + diff --git a/docs/pyspos.html b/docs/pyspos.html index 9a2f547..5ad99c8 100644 --- a/docs/pyspos.html +++ b/docs/pyspos.html @@ -7,7 +7,7 @@ PySpOS | GoutouStdio - + @@ -80,7 +80,7 @@

PySpOS

-

用 Python 写的模拟操作系统。它跑在宿主系统上,自己实现了 shell、进程调度、文件系统、引导流程和 OTA 更新,也能执行 x86_64 的 ELF 程序。

+

用 Python 写的模拟操作系统,跑在宿主系统上。有 shell、进程调度、文件系统和 OTA 更新,还能执行 x86_64 ELF 程序。

下载 3.2.0 查看源码 @@ -105,140 +105,84 @@

PySpOS

0 0 Sleeping idle swapper/0 1 0 Sleeping init init 2 1 Running shell pyspos-sh -
shell 中 help 与 ps 的输出(节选)。格式取自 src/commands.py、src/shell/proc_cmds.py 和 src/process.py;PID 0、1、2 是保留进程。
+
shell 里 help 和 ps 的输出(节选)。PID 0、1、2 是保留进程。
-
-
-
    -
  • 真子进程

    应用是独立的 OS 进程,有 PID、父子关系和信号。

  • -
  • EEVDF 调度

    参照 Linux 6.6+,按虚拟截止期和 nice 权重排程。

  • -
  • ELF 引擎

    Unicorn 执行 x86_64 指令,缺它时退回自研模拟器。

  • -
  • A/B 升级

    更新装进非活动槽位,校验后切换,失败可回滚。

  • -
-
-
+
+
+ +
+

简介

+

PySpOS 从 2022 年的“MP4 架构狗头 OS”演变而来。应用是独立的进程,调度器参照 Linux 的 EEVDF。

+
    +
  • 真子进程:应用有 PID、父子关系和信号。
  • +
  • EEVDF 调度:按虚拟截止期和 nice 权重排程。
  • +
  • ELF 引擎:Unicorn 执行 x86_64 指令;没装时退回自研的 SpaceCPU 1 Pro。
  • +
  • A/B 升级:更新装进非活动槽位,校验后切换,失败可回滚。
  • +
+

动态链接(INTERP)和 TLS 还没有实现。

-
-
-
-

一条命令的执行路径

-

以外部程序为例:从按下 Enter 到系统调用,经过这三步。

-
-
    -
  1. -
    1
    -

    Shell:parse → dispatch

    -

    Shell 把输入解析成命令和参数,再交给对应的模块。管道、重定向和后台作业都从这里开始。

    - src/shell/dispatch.py -
  2. -
  3. -
    2
    -

    进程:fork → exec

    -

    外部应用通过 fork / exec 运行,是独立的进程。调度器用虚拟运行时间和截止期安排执行。

    - src/forkexec.py · src/process.py -
  4. -
  5. -
    3
    -

    运行时:execute → syscall

    -

    ELF 由 Unicorn 执行 x86_64 指令,系统调用转发到内置的模拟层。可以观察执行、改实现,再运行一次。

    - Unicorn Engine → syscall -
  6. -
-
-
+

一条命令怎么执行

+
    +
  1. Shell:解析输入并分发给对应模块。管道、重定向和后台作业从这里开始。
  2. +
  3. 进程:外部程序经 fork / exec 成为独立进程,由调度器安排运行。
  4. +
  5. 运行时:Unicorn 执行 ELF 指令,系统调用转给内置的模拟层。
  6. +
-
-
-

概览

-
-

PySpOS 从 2022 年的“MP4 架构狗头 OS”一路改过来。现在的版本能跑命令、管理文件、执行 ELF 程序;应用是独立进程,调度器参照 Linux 的 EEVDF。

-

3.2.0 重构了内部结构:原先 1217 行的 main.py 拆成 src/shell/ 下的 sys、elf、ota、proc、dispatch 几个模块,原文件留作兼容层。调度在 process.py,进程引擎在 forkexec.py。

-

ELF 默认由 Unicorn Engine 执行真实的 x86_64 指令,系统调用转发给内置的模拟层。没装 unicorn 时退回自研的 SpaceCPU 1 Pro 模拟器,Windows 上也能跑。动态链接(INTERP)和 TLS 还没有实现。

-
-
硬件要求状态
- - - - - - - - -
当前版本v3.2.0 RC1
项目启动2022
核心语言Python(另有部分 C 测试程序)
升级方式A/B 槽位,可在本地回滚
程序格式ELF(x86_64)、SPF 脚本
测试按子系统分组,在 GitHub Actions 上运行
-
-
-
- +

3.2.0 更新

+

这一版把 1217 行的 main.py 拆进了 src/shell/,原文件保留为兼容层。

+
    +
  • 进程:不再是协作式模拟。PID 0、1、2 依次引导,孤儿进程由 init 收养。
  • +
  • 调度:EEVDF,参照 Linux 6.6+ 的权重表、vruntime 和虚拟截止期。
  • +
  • 后台作业:cmd &、jobs、fg、bg、wait,以及进程组与会话。
  • +
  • ELF 调试:加 --stats、--map、--disasm、--strace 观察执行过程。
  • +
  • 窗口:VortexGlass 管理窗口和输入,应用可以用 Python SDK 开窗口、画控件,见 vortexglass.md。
  • +
  • 开机向导:11 步 curses 向导,语言和时区会实际生效,用 oobe 重跑。
  • +
  • 终端:中文按双宽排版,修好了按 Enter 出现 ^M 的问题。
  • +
  • 命令:分组 help、Tab 补全、管道和重定向;apps/ 下的程序可以直接执行。
  • +
  • 配置:SpaceConfig v2(.spc),带 Schema 校验和迁移工具。
  • +
  • OTA:云端已恢复,Recovery 里可以选版本装到指定槽位。
  • +
  • 出厂重置:recovery.erase 清理 8 类数据。
  • +
  • 许可:改用 MIT。
  • +
-
-
-

3.2.0 的改动

-
    -
  • 进程

    应用是独立的 OS 进程,不再是协作式模拟。PID 0(swapper)、1(init)、2(shell)依次引导;孤儿进程由 init 收养,退出后先成为僵尸,再被回收。

  • -
  • 调度

    EEVDF(Earliest Eligible Virtual Deadline First),参照 Linux 6.6+:nice 权重表、vruntime、虚拟截止期和信号状态机。

  • -
  • 后台作业

    cmd &、jobs、fg、bg、wait、$!,以及进程组与会话。前台交互经管道中继,后台作业的 stdin 接 /dev/null。

  • -
  • ELF 引擎

    默认用 Unicorn,没装就用 SpaceCPU 1 Pro。可以加 --stats、--map、--disasm、--strace 观察执行过程。

  • -
  • 窗口 API

    VortexGlass 管理窗口和输入。应用可以用 Python SDK 开多个窗口、处理键盘鼠标事件、绘制控件,或直接提交 RGBA 像素。记事本、文件浏览器和绘图程序各自持有界面和逻辑。API 与示例见 vortexglass.md。

  • -
  • 首次开机向导

    共 11 步的 curses 向导。语言和时区设置会实际生效(zoneinfo + TZ),完成标记写在 etc/.oobe_done,可以用 oobe 命令重跑。

  • -
  • 终端

    TUI 按显示宽度排版(CJK 双宽),并去掉完整的 ANSI 转义序列。输入侧有 stty sane 恢复、输入归一化和重试上限,按 Enter 出现 ^M 的问题已修复。

  • -
  • 命令

    命令走元数据注册表:分组 help、help <cmd>、Tab 补全,支持管道和重定向。PATH=apps/ 下的外部程序可以直接执行。

  • -
  • SpaceConfig v2

    .spc 配置格式,支持转义、行尾注释、null 和列表,带 Schema 校验,以及 spc_get / set / migrate 工具。

  • -
  • A/B 槽位与回滚

    更新装进非活动槽位,校验通过后再切换,失败可以回滚。OTA 云端已恢复(goutoustdio.rainyland.top),在 Recovery 里也可以从云端选版本装到指定槽位。

  • -
  • 出厂重置

    recovery.erase 清理 8 类数据,包括题库历史、shell 历史和整棵目录树的 __pycache__。

  • -
  • SPF 脚本

    SPF(Space Portable Format)是自研的脚本格式,支持变量、算术、输入和引入。

  • -
  • 许可与测试

    3.2.0 起使用 MIT 许可,代码在 ViudiraTech/PySpOS。回归测试在 GitHub Actions 上运行。

  • -
-
-
- -
-
-
-

模块

-

每个模块只列一行,完整说明在仓库 README 和源码注释里。

-
-
+

源码结构

- + - - - - - - + + + + + + + +
模块文件作用
位置作用
主循环src/kernel.py处理用户输入、执行命令、管理应用运行,启动时初始化各子系统。入口是 kernel.loop()。
SPF 解析src/parse_spf.py解析并执行 SPF 文件,支持 putchar() 与 exit() 系统调用。
OTA 更新src/ota.py版本检查、下载、验证、安装,云端地址是 goutoustdio.rainyland.top。
文件系统src/fs.pylist / create / read / delete 等文件操作 API,基于 os 和 shutil 实现。
配置管理src/btcfg.py管理启动配置(ROOT 权限、启动器锁定状态),存放在 etc/bootcfg.json。
ELF 运行src/elf_loader/ELF 加载、CPU 模拟和系统调用模拟,支持 x86 / x86_64。用法:run program.elf,可加 --stats / --map / --strace。
src/kernel.py主循环:处理输入、执行命令、初始化子系统
src/shell/命令解析与分发
src/process.py进程与调度
src/elf_loader/ELF 加载、CPU 与系统调用模拟
src/parse_spf.pySPF 脚本(自研格式)
src/ota.py更新的检查、下载、校验和安装
src/fs.py文件操作 API
src/btcfg.py启动配置,存在 etc/bootcfg.json
-
-
-
-
-
-

兼容性

-
-
- - - - - - - -
WindowsWindows 7 及以上,兼容性最好
Linux主流发行版(Ubuntu、Debian、Fedora 等)
macOSmacOS 10.14 及以上
Python3.10 及以上;CI 覆盖 3.10、3.12、3.14
-
- -
+

运行环境

+
    +
  • Python:3.10 或更高。
  • +
  • 系统:Windows 7+、主流 Linux 发行版、macOS 10.14+。
  • +
+

详见配置要求。更新包在下载页,源码在 ViudiraTech/PySpOS。

+
@@ -289,7 +233,7 @@ - + diff --git a/docs/requirements.html b/docs/requirements.html index 549df5e..b29a086 100644 --- a/docs/requirements.html +++ b/docs/requirements.html @@ -7,7 +7,7 @@ 配置要求 | GoutouStdio - + @@ -223,7 +223,7 @@ - + diff --git a/docs/spaceos6.html b/docs/spaceos6.html index c5ea896..ce31c11 100644 --- a/docs/spaceos6.html +++ b/docs/spaceos6.html @@ -7,7 +7,7 @@ SpaceOS 6(已停更) | GoutouStdio - + @@ -80,7 +80,7 @@

已停更

SpaceOS 6

-

基于 Uinxed-Kernel 的桌面操作系统:SPUI 1.0 界面、SPF Parser 2.0、SPFS 2.0 文件系统。这是团队第一次接触 UEFI。

+

基于 Uinxed-Kernel 的桌面系统,带 SPUI 1.0 界面和 SPFS 2.0 文件系统。这是团队第一次接触 UEFI。

版本
6.0
首次发布
2024
@@ -91,54 +91,50 @@

SpaceOS 6

-
-
-
- -
-

这个项目已停止开发,本页仅作存档

-

不再维护,不接受 issue、功能请求或移植,请不要基于它继续投入。目前在维护的是 PySpOS 和 SpaceOS 9;PySpOS 的代码在 ViudiraTech/PySpOS。

- -
+
+
+ +
+
+ +
+

这个项目已停止开发,本页仅作存档

+

不再维护,不接受 issue 或功能请求。目前在维护的是 PySpOS 和 SpaceOS 9。

-
-
-
-

背景

-
-

SpaceOS 6 基于 ViudiraTech 开发的 Uinxed-Kernel 深度定制,2024 年开始研发,内核优化、用户界面、文件系统和硬件驱动都是自己做的。

-

Uinxed-Kernel 是 64 位宏内核,运行在 CPU Ring 0,进程调度、内存管理、文件系统和设备驱动都在内核空间。SPUI(SpaceOS User Interface)1.0 是自研的桌面环境。硬件上支持 AHCI / NVMe 存储、SB16 声卡、JPG 硬件加速和分区表,可以多系统部署。

-
-
-
+

背景

+

2024 年开始,在 ViudiraTech 的 Uinxed-Kernel 上深度定制。界面、文件系统和驱动是自己做的。

-
-
-

做到的部分

-
    -
  • SPUI 1.0 桌面

    自研桌面环境,支持窗口管理、任务栏和开始菜单。

  • -
  • SPF Parser 2.0

    解析并执行 SPF(SpaceOS Format)可执行文件。2.0 版性能更好,支持更多系统调用。

  • -
  • JPG 图片解析

    原生支持 JPG 解析与显示,不依赖第三方库。

  • -
  • SB16 声卡

    SB16 声卡驱动,支持 PCM 播放和 MIDI 合成。

  • -
  • 分区表与存储

    分区表、AHCI、NVMe,多分区管理和引导加载器配置。

  • -
  • SPFS 2.0

    自研文件系统,带文件权限管理和日志,面向硬盘优化读写延迟和吞吐。

  • -
-
-
+

做到的部分

+
    +
  • SPUI 1.0:自研桌面,有窗口管理、任务栏和开始菜单。
  • +
  • SPF Parser 2.0:执行 SPF 可执行文件,支持更多系统调用。
  • +
  • SPFS 2.0:自研文件系统,带权限管理和日志。
  • +
  • 存储:IDE / AHCI / NVMe,MBR 与 GPT 分区表,可以多系统部署。
  • +
  • 音频:SB16 驱动,支持 PCM 播放和 MIDI。
  • +
  • 图片:原生 JPG 解析,不依赖第三方库。
  • +
-
-
-

内核与子系统

-
    -
  • Uinxed-Kernel

    ViudiraTech 开发的 64 位宏内核,运行在 Ring 0,提供系统调用、进程调度、内存管理和文件系统。

  • -
  • 宏内核架构

    进程管理、内存管理、文件系统和设备驱动都在内核空间。

  • -
  • 内存管理

    分页和内存保护。

  • -
  • 存储

    IDE / AHCI / NVMe 接口,MBR 与 GPT 分区表。

  • -
  • 音频

    SB16 兼容驱动,DMA 传输,音频混合与音量控制。

  • -
  • 当年的配置要求

    x86 处理器(Pentium 及以上),内存最低 64 MB(推荐 128 MB 以上),至少 500 MB 可用空间,VESA 兼容显卡(800×600)。

  • -
+

内核

+

Uinxed-Kernel 是 64 位宏内核,运行在 Ring 0。调度、内存、文件系统和驱动都在内核空间,内存有分页和保护。

+ +

当年的配置要求

+
    +
  • Pentium 及以上的 x86 处理器。
  • +
  • 内存 64 MB,推荐 128 MB 以上。
  • +
  • 500 MB 可用空间。
  • +
  • VESA 兼容显卡,800×600。
  • +
+
@@ -189,7 +185,7 @@
- + diff --git a/docs/spaceos7.html b/docs/spaceos7.html index b56f7cc..ef2b59d 100644 --- a/docs/spaceos7.html +++ b/docs/spaceos7.html @@ -7,7 +7,7 @@ SpaceOS 7(已停更) | GoutouStdio - + @@ -89,65 +89,49 @@

SpaceOS 7

-
-
-
- -
-

这个项目已停止开发,本页仅作存档

-

不再维护,不接受 issue、功能请求或移植,请不要基于它继续投入。目前在维护的是 PySpOS 和 SpaceOS 9;PySpOS 的代码在 ViudiraTech/PySpOS。

- -
-
-
-
- -
-
-

背景

-
-

2026 年,团队研究清楚 UEFI 之后启动了这个项目:没有外部代码,没有依赖,UEFI 引导和内核加载全部自己写。设计目标写的是“完全自研、混合内核、极致性能”,混合内核想兼顾微内核的稳定和宏内核的性能。

-

做到约一半时停止:用户态没做完,还存在 Heisenbug。中间重构过一次引导和内核;桌面环境运行在 Ring 3,桌面崩溃不会拖垮内核。

+
+
+ +
+
+ +
+

这个项目已停止开发,本页仅作存档

+

不再维护,不接受 issue 或功能请求。目前在维护的是 PySpOS 和 SpaceOS 9。

-
-
-
-

做到的部分

-
    -
  • SPFS 2.0 Ultra

    自研文件系统,带 Journal 日志、CRC64 数据校验和缓存块机制。

  • -
  • 内核重构

    引导程序自己写(没用 Limine / GRUB);放弃 Uinxed-Kernel,改为纯自研;伙伴系统加 Slab 分配器;进程间 IPC 和用户态支持。

  • -
  • Ring 3 桌面

    桌面跑在用户空间,与内核隔离,桌面崩溃不影响内核。

  • -
  • SMP 多核

    多核调度、同步机制、内存管理和中断处理。

  • -
  • xHCI 控制器

    xHCI 驱动,支持 USB 3.0 及以上,向后兼容 USB 2.0。

  • -
  • 空间 API

    自研的系统调用接口,包含系统调用、IPC 和权限控制。

  • -
-
-
+

背景

+

2026 年搞懂 UEFI 后启动:不用外部代码,引导和内核加载全部自己写。目标是混合内核,兼顾微内核的稳定和宏内核的性能。

+

做到约一半时停止:用户态没完成,还有难以复现的 Heisenbug。

-
-
-

内核设计

-
    -
  • 混合内核

    微内核组件放到用户空间,关键服务留在内核空间;有 IPC 和权限管理。

  • -
  • UEFI 库

    从零写的 UEFI 库,高半地址内核加载,不依赖外部引导代码。

  • -
  • 内存管理

    Buddy System 加 Slab 分配器,4 KB 与 2 MB 大页,虚拟内存和硬件辅助的内存保护。

  • -
  • 进程调度

    CFS 完全公平调度,支持实时调度、多核负载均衡与节能调度。

  • -
  • 设备驱动

    存储(IDE / AHCI / NVMe / USB / SCSI)、显示、网络、音频、输入,以及 xHCI USB 驱动。

  • -
-
-
+

做到的部分

+
    +
  • 自写引导:没用 Limine 或 GRUB,也不再基于 Uinxed-Kernel。
  • +
  • 混合内核:微内核组件放在用户空间,关键服务留在内核,带 IPC 和权限控制。
  • +
  • 内存:Buddy + Slab 分配器,4 KB 与 2 MB 页。
  • +
  • 调度:CFS,支持实时调度和多核负载均衡。
  • +
  • Ring 3 桌面:桌面跑在用户空间,崩溃不影响内核。
  • +
  • SPFS 2.0 Ultra:自研文件系统,带日志和 CRC64 校验。
  • +
  • 驱动:xHCI(USB 3.0+)、存储、显示、网络、音频和输入。
  • +
  • 空间 API:自研的系统调用、IPC 和权限接口。
  • +
-
-
-

进展

-
    -
  1. 搞懂 UEFI

    理解 UEFI 之后,启动完全自研的操作系统项目。(用户态没做好)

  2. -
  3. 混合内核设计

    完成混合内核的架构设计。

  4. -
  5. 内核实现

    完成内核基本功能:进程调度、内存管理、中断处理。

  6. -
  7. Ring 3 桌面

    实现用户空间的桌面环境。

  8. -
+

进展

+
    +
  1. 搞懂 UEFI,启动项目。
  2. +
  3. 完成混合内核的架构设计。
  4. +
  5. 实现调度、内存管理和中断处理。
  6. +
  7. 实现 Ring 3 桌面,之后停止。
  8. +
+
@@ -198,7 +182,7 @@
- + diff --git a/docs/spaceos8-download.html b/docs/spaceos8-download.html index 0284059..0704cad 100644 --- a/docs/spaceos8-download.html +++ b/docs/spaceos8-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 8 | GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/spaceos8.html b/docs/spaceos8.html index a9e16a2..0a8e1f4 100644 --- a/docs/spaceos8.html +++ b/docs/spaceos8.html @@ -7,7 +7,7 @@ SpaceOS 8 | GoutouStdio - + @@ -81,7 +81,7 @@

抢先体验 上一代

SpaceOS 8

-

内核是 C 写的 UNIX-like 内核 Uinxed-Kernel,用户态是一套自研的桌面和系统程序,包括窗口合成器、浏览器和文件管理器。第二版 slim 镜像修好了 xHCI 上的 USB HID,USB 键盘和鼠标现在可以用;第一版里它们插上没有反应。

+

内核是 C 写的 UNIX-like 内核 Uinxed-Kernel,用户态是一整套自研的桌面和系统程序。第二版镜像修好了 xHCI 上的 USB 键盘和鼠标。

下载第二版镜像 内核源码 @@ -94,205 +94,134 @@

SpaceOS 8

Minecraft 在 Weston 桌面中运行的截图,世界完整渲染,生命值、饥饿条、快捷栏和准星都正常 -
Minecraft 运行在 Uinxed 内核上:用户态是 Alpine Linux v3.24,桌面是 Weston(Wayland),终端是 weston-terminal 14.0.2,宿主是 QEMU/KVM Q35。窗口、输入和渲染正常。
+
Minecraft 运行在 Uinxed 内核上(Alpine Linux v3.24 + Weston,QEMU/KVM)。
-
-
-
    -
  • 自研用户态

    桌面、合成器、浏览器和系统程序,约 29.2 万行。

  • -
  • X11 兼容层

    120 个核心协议请求,能运行 DOOM 这类图形程序。

  • -
  • Uinxed 内核

    C 写的 UNIX-like 内核,实现 Linux 6.12 系统调用 ABI。

  • -
  • 实体机验证

    已在 Dell PowerEdge R410 等实体服务器上启动。

  • -
-
- -
-

抢先体验版,不是正式 release

-

内核已在实体服务器上启动,桌面能运行,也能跑 OpenJDK 和 DOOM 这类现成的 Linux 软件。图形和部分 ABI 还在补,所以只叫抢先体验。

- -
+
+
+ +
+
+ +
+

抢先体验版,不是正式发布

+

内核已在实体服务器上启动,桌面能运行;图形和部分 ABI 还在补。

-
-
-
-

系统结构

-
-

从上到下是各层的职责;虚线是用户态和内核之间的系统调用接口。

-
    -
  1. SPUI 桌面与应用compositor · browser · terminal · filemgr · settings 等
  2. -
  3. X11 兼容层 · VortexGlassLinux 图形软件与窗口主题
  4. -
  5. libc · libgfx · imgui自研的 C 库、图形库和 GUI 工具包
  6. -
  7. Linux 6.12 系统调用 ABI编号 0–462
  8. -
  9. Uinxed-KernelEEVDF 调度 · 内存 · VFS · 网络 · 驱动
  10. -
  11. Limine 引导UEFI 与 Legacy 双模
  12. -
-
-
-
+

简介

+
    +
  • 用户态约 29.2 万行,内核约 18.1 万行,全部自研。
  • +
  • 内核实现 Linux 6.12 的系统调用 ABI(编号 0–462)。
  • +
  • 已在 Dell PowerEdge R410 等实体服务器上启动。
  • +
  • 内核仓库使用 Apache-2.0 许可。
  • +
-
-
-

规模

-
- - - - - - - - - - - - -
部分规模说明
用户态约 29.2 万行SPUI 桌面、合成器、浏览器和系统程序,全部自研。
内核约 18.1 万行C 写的 UNIX-like 单体内核,449 个 C/H 文件。
图形界面约 3.4 万行合成器和自研图形库 libgfx。
浏览器约 2.4 万行纯 C 的 UxHtml 引擎,带 JavaScript 引擎。
系统调用编号 0–462Linux 6.12 x86-64 的系统调用编号。
版本 / 许可1.0.0-alpha.1内核仓库的许可是 Apache-2.0。
-
-
-
+

系统结构

+

从上到下:

+
    +
  1. SPUI 桌面与应用:合成器、浏览器、终端、文件管理器等。
  2. +
  3. X11 兼容层与 VortexGlass 窗口主题。
  4. +
  5. 自研库:libc、libgfx、imgui。
  6. +
  7. Linux 6.12 系统调用 ABI:用户态和内核的分界。
  8. +
  9. Uinxed-Kernel:调度、内存、VFS、网络、驱动。
  10. +
  11. Limine 引导:UEFI 与 Legacy 都支持。
  12. +
-
-
-

现成软件的运行情况

-
-

下面这些程序都不是为这个内核写的。

+

能跑的现成软件

- + - - - - + + + +
程序状态备注
程序状态说明
OpenJDK能运行为它重写了信号处理、TSS / SMP / APIC 和中断路径,并修了 VFS 与页管理。JVM 的 GC 线程、线程挂起和内存映射都依赖这些地方行为正确。
DOOM能运行经自研的 X11 兼容层运行。需要键盘、鼠标、连续动画帧、音频和稳定的显示通路。
systemd初步跑通systemd 一启动就探测大量可选的 sysctl、挂载点和命名空间接口,任何一处行为不符都会起不来。为此补了 Cgroup v2 和完整的 Linux 命名空间支持。
Minecraft团队实测需要内核扛住真实 JVM 的内存管理、GC 线程调度、mmap 和 futex 行为。运行环境见页首截图。
OpenJDK能运行为它重写了信号、SMP 和中断路径。
DOOM能运行经自研的 X11 兼容层运行。
systemd初步跑通为它补了 Cgroup v2 和命名空间。
Minecraft团队实测见页首截图。
-
-
-
- -
-
-

X11 兼容层与窗口主题

-
    -
  • X11 兼容层

    从零写的 X11 服务器,实现了 120 个核心协议请求,约 4500 行。目的是让原生 SPUI 桌面直接运行 Linux 上的图形软件:窗口映射、绘制、输入与事件、字体、光标、属性与选区,缺任何一个都会让部分程序起不来。

  • -
  • VortexGlass

    Vista Aero 风格的毛玻璃窗口装饰,在自研合成器里逐像素绘制。34 个素材覆盖四边边框、标题栏按钮的四种状态和 Aero orb。主题可以在 tech / win98 / vortexglass 之间切换,由配置服务统一管理,所有应用共用一套外观。

  • -
-
-
-
-
-

用户态与桌面

-
-

用户态没有套现成的发行版:从窗口合成器到浏览器都是自己写的。

-
    -
  • SPUI 桌面

    约 4500 行,14 个文件。有 dock、壁纸和窗口管理;dock 上可以启动终端、开始屏幕、文件管理器、活动监视器和文本编辑器。图标缺失时有降级配色。

  • -
  • 服务与配置

    init / service / uxconfd。自研的 PID 1 服务管理器,有目标里程碑、进程监管与自动重启、僵尸回收。状态输出对齐 systemd 的 [ OK ] Started … 格式,提供 systemctl 风格的命令行,经 /tmp/initctl 与 PID 1 通信。配置由 uxconfd 统一管理,用 uxconf-query 读写。

  • -
  • 首次开机向导

    约 2100 行。思路和 PySpOS 里的向导相同:语言、时区等设置会实际生效。

  • -
  • libc 与工具链

    libc / libgfx / imgui:自己的 C 库、图形库和 GUI 工具包,另有自研的 sh。

  • +

    桌面与应用

    +
      +
    • SPUI 桌面:dock、壁纸和窗口管理。
    • +
    • X11 兼容层:从零写的 X11 服务器,实现 120 个核心协议请求。
    • +
    • VortexGlass:Aero 风格的毛玻璃窗口,主题可在 tech / win98 / vortexglass 间切换。
    • +
    • init:自研 PID 1,带服务监管和 systemctl 风格的命令。
    • +
    • libc / libgfx / imgui:自己的 C 库、图形库和 GUI 工具包。
    -

    应用

    -

    全部自研,不是移植来的。行数是源码规模。

    +

    代码量最大的几个应用:

    - - - - - - - - - - - - - - - + + + + +
    程序作用行数
    compositor窗口合成器:DRM/KMS、损伤跟踪、Xcursor、窗口装饰12,320
    browser浏览器,含 UxHtml 渲染引擎与 JS 引擎25,713
    libgfx自研图形库与控件库21,785
    settings系统设置3,064
    terminal终端仿真器2,869
    oobe首次开机向导2,137
    editor文本编辑器1,637
    displaymgr显示管理器1,489
    filemgr文件管理器1,347
    sysmon活动监视器1,267
    initPID 1 服务管理器1,262
    uxconfd配置守护进程873
    netmgrd网络管理器761
    escalate提权通道685
    calc计算器388
    browser浏览器,含 UxHtml 渲染引擎和 JS 引擎25,713
    libgfx图形库与控件库21,785
    compositor窗口合成器12,320
    settings系统设置3,064
    terminal终端仿真器2,869
    -
-
-
+

另有开机向导、编辑器、文件管理器、活动监视器、计算器等,都是自研的。

-
-
-

Uinxed-Kernel

-
-

桌面、应用和浏览器都跑在这个内核上:C 从零写的 UNIX-like x86_64 单体内核,经 Limine 引导(UEFI 与 Legacy 双模),实现 Linux 6.12 的系统调用 ABI。

-
- - - - - - - - - -
调度与进程EEVDF 调度器,每 CPU 运行队列配红黑树时间线(vruntime / deadline / vlag / weight);SMP 任务迁移与负载均衡、IPI 抢占;两阶段等待队列避免丢失唤醒;互斥量与 futex 的优先级继承;cgroups、ptrace、可加载内核模块。
内存管理二元 buddy 物理页分配器,四级页表(4 KiB / 2 MiB / 1 GiB);高半区直接映射(HHDM)和 slab 分配器;统一页缓存,带 LRU 回收、脏页回写和预读;swap 子系统。
文件系统UNIX 风格的 VFS,有挂载点和 inode 节点;tmpfs 作默认根,另有 procfs、sysfs、devtmpfs、cpio、cgroupfs;实际文件系统覆盖 FAT12/16/32、exFAT、ext2/3/4、NTFS(可写)和 ISO 9660。
网络自研协议栈:Ethernet、ARP、IPv4/IPv6、ICMP/ICMPv6、NDP、UDP、TCP;e1000/e1000e 与 RTL8139/8169 网卡驱动;Linux 套接字 ABI、DHCP 客户端,以及 /proc/net 和 /sys/class/net 视图。
驱动输入:PS/2 键鼠、evdev、USB HID。存储:IDE/ATA、AHCI、NVMe、USB Mass Storage。显示:DRM/KMS、VirtIO-GPU、GOP 帧缓冲。音频:SB16、Intel HD Audio。总线与平台:PCI/PCIe、USB、I2C、ACPI、HPET、TPM 1.2/2.0。
安全与 IPCseccomp 过滤和 no_new_privs;AF_UNIX / AF_NETLINK / AF_INET / AF_INET6;pipe、epoll、eventfd、timerfd、signalfd、memfd、pidfd、POSIX 消息队列和 System V IPC;futex2;inotify;Unix98 PTY 和多虚拟终端。
-
-
-
-
+

Uinxed-Kernel

+
    +
  • 调度:EEVDF,SMP 负载均衡,futex 优先级继承。
  • +
  • 内存:buddy + slab,四级页表,页缓存和 swap。
  • +
  • 文件系统:VFS;支持 FAT、exFAT、ext2/3/4、NTFS 和 ISO 9660。
  • +
  • 网络:自研 TCP/IP 协议栈(IPv4 / IPv6),e1000 和 RTL8139/8169 网卡驱动。
  • +
  • 驱动:USB HID、AHCI、NVMe、DRM/KMS、VirtIO-GPU、HD Audio、ACPI。
  • +
  • 安全与 IPC:seccomp、epoll、eventfd、memfd、PTY 等。
  • +
-
-
-

当前状态

-
-
- -
-

实验性系统,不要用重要数据冒险

-

内核版本串是 Uinxed 1.0.0-alpha.1。请用可抛弃的机器和测试盘。

- -
+

当前状态

+
+ +
+

实验性系统,不要拿重要数据冒险

+

请用可抛弃的机器和测试盘。NTFS 写入驱动还不能信任。

+

已经跑通

    -
  • 内核在实体服务器上启动,已验证 Dell PowerEdge R410 等机器。
  • -
  • 能引导 Alpine Linux 并拉起图形桌面。Weston(Wayland)和 Xfce 两条路径都验证过,终端可用。
  • -
  • SPUI 桌面和自研用户态在同一个内核上运行。
  • -
  • X11 兼容层能运行 DOOM 这类图形程序,OpenJDK 能运行。
  • +
  • 在实体服务器上启动。
  • +
  • 引导 Alpine Linux 并拉起图形桌面,Weston 和 Xfce 都验证过。
  • +
  • SPUI 桌面和自研用户态。
  • +
  • DOOM 和 OpenJDK。
-

第二版的更新

-

修复 xHCI 控制器上的 USB HID,USB 键盘和鼠标可以使用。第一版镜像继续保留,详情和校验值见下载页的版本记录。

还在做

    -
  • GPU 驱动、虚拟化设备、音频,以及相当一部分 Linux ABI。
  • -
  • PS/2 输入路径、evdev 消费者、poll/epoll 唤醒和 EEVDF 调度器还在验证阶段。
  • -
  • systemd 只是初步跑通,离可用还有距离。
  • -
  • NTFS 写入驱动还不能信任。
  • +
  • GPU 驱动、音频和部分 Linux ABI。
  • +
  • PS/2 输入、epoll 唤醒和调度器的验证。
  • +
  • 让 systemd 真正可用。
-
-
-
+

第二版镜像修复了 xHCI 上的 USB HID,第一版仍然保留,见版本记录。

-
-
-

构建与运行

-
-

需要 make、gcc(建议 13.3+)、qemu、xorriso;menuconfig 另需 kconfig-frontends 和 libncurses-dev。

-

在 QEMU 里运行

-

不需要额外硬件,最快能看到桌面。make run 已经把串口输出接到终端。要抓内核日志,把 limine.conf 里的 console=tty0 改成 console=ttyS0:启动挂死时屏幕控制台可能丢日志,串口更可靠。

-

在实体机上运行

-

已在 Dell PowerEdge R410 上验证。先看仓库 README 的 Run on physical hardware 一节,按步骤准备引导介质。请用可抛弃的机器和测试盘;文件系统驱动(尤其是 NTFS 写入)目前不适合放重要数据。

-

源码 · 配置要求 · 下载镜像

-
+

构建与运行

+

需要 make、gcc(建议 13.3+)、qemu 和 xorriso。

+
    +
  • QEMU:make run,串口输出直接到终端。要抓内核日志,把 limine.conf 里的 console=tty0 改成 console=ttyS0。
  • +
  • 实体机:按仓库 README 的 Run on physical hardware 一节准备引导介质。
  • +
+

内核源码 · 配置要求 · 下载镜像

+
@@ -343,7 +272,7 @@
- + diff --git a/docs/spaceos9-download.html b/docs/spaceos9-download.html index cc763ae..ce03ed4 100644 --- a/docs/spaceos9-download.html +++ b/docs/spaceos9-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 9 | GoutouStdio - + @@ -232,7 +232,7 @@ - + diff --git a/docs/spaceos9.html b/docs/spaceos9.html index 61436be..2cea06e 100644 --- a/docs/spaceos9.html +++ b/docs/spaceos9.html @@ -7,7 +7,7 @@ SpaceOS 9 | GoutouStdio - + @@ -81,7 +81,7 @@

公开预览

SpaceOS 9

-

内核是 Rust 写的 SpaceKernel 3.0,实现了 Linux 64 位 ABI,目标是让现成的 Linux 程序直接运行。目前验证过的是 musl 动态链接的 BusyBox。镜像是 x86_64 的可启动 ISO。

+

内核 SpaceKernel 3.0 用 Rust 写成,实现 Linux 64 位 ABI,目标是直接运行现成的 Linux 程序。目前验证过 musl 动态链接的 BusyBox。

下载预览镜像 查看源码 @@ -99,102 +99,72 @@

SpaceOS 9

-
-
-
    -
  • Rust 内核

    SpaceKernel 3.0,soft-float,x86_64 优先。

  • -
  • Linux ABI

    ELF / PIE / 动态链接、信号、futex、epoll。

  • -
  • FAT32 根盘

    由 Limine 模块提供,只读挂载。

  • -
  • BusyBox ash

    musl 动态链接,带作业控制和 Ctrl-C。

  • -
-
- -
-

这是公开预览

-

内核自测、三个架构的编译检查和 QEMU 启动已经通过;用户态和驱动覆盖还在补。下载前请读使用声明。

- -
+
+
+ +
+
+ +
+

这是公开预览

+

内核自测、三个架构的编译检查和 QEMU 启动已经通过,用户态和驱动还在补。下载前请读使用声明。

-
-
-
-
-

启动顺序

-

在 QEMU / UEFI 上的启动流程,每一步对应镜像里的一个组件。

-
-
    -
  1. 1

    UEFI 固件

    QEMU 里由 OVMF 提供,从 ISO 启动引导程序。

  2. -
  3. 2

    Limine

    引导内核,并把 FAT32 的 rootfs 作为模块交给它。

  4. -
  5. 3

    SpaceKernel 3.0

    初始化后只读挂载根盘,再启动 PID 1。

  6. -
  7. 4

    systemg

    作为 PID 1,按依赖顺序启动 .service 里定义的服务。

  8. -
  9. 5

    ash

    控制台 shell。退出后由 systemg 重新拉起。

  10. -
-
-
+

组成

+
    +
  • 内核:SpaceKernel 3.0,Rust、soft-float,x86_64 优先。
  • +
  • ABI:Linux 64 位,支持 ELF / PIE / 动态链接、信号、futex、epoll、AF_UNIX。
  • +
  • 根盘:FAT32,由 Limine 模块提供,只读挂载。
  • +
  • PID 1:systemg,按依赖启动服务并回收进程。
  • +
  • 用户态:自带 C 库 splibc 和 sh;BusyBox 通过 musl 动态链接运行。
  • +
  • 终端:ash,支持作业控制和 Ctrl-C / Ctrl-Z。
  • +
+ +

启动顺序

+
    +
  1. UEFI 固件(QEMU 里是 OVMF)从 ISO 启动引导程序。
  2. +
  3. Limine 加载内核,并把 FAT32 根盘作为模块交给它。
  4. +
  5. SpaceKernel 初始化,只读挂载根盘,启动 PID 1。
  6. +
  7. systemg 按 .service 的依赖启动服务。
  8. +
  9. 拉起控制台 shell ash;退出后由 systemg 重启。
  10. +
+ +

这一版新增

+
    +
  • VFS 缓存:Linux 语义的 dentry / inode 缓存和 LRU 回收;FAT32 大文件读取加了簇链缓存。
  • +
  • 动态链接:内核装载 ELF 解释器和 PIE,能运行 musl 动态链接的 BusyBox。
  • +
  • 进程与信号:线程、进程组与会话、POSIX 信号、futex、epoll、signalfd、timerfd。
  • +
  • 终端:控制终端、前后台进程组和 raw 模式。
  • +
-
-
-

组成

-
-

SpaceOS 9 没有另造一套用户接口,而是尽量把 Linux 现有的进程、文件和终端语义做完整,让现成程序能直接运行。

+

验证情况

+ + + - - - - - - + + +
范围状态说明
内核SpaceKernel 3.0。Rust、soft-float、多架构,x86_64 优先。
ABILinux 64 位:ELF / PIE / PT_INTERP、系统调用、信号、futex、epoll、AF_UNIX。
根盘FAT32。Limine 模块提供 rootfs,只读挂载;有 /dev、/proc、/sys、/tmp、/run。
PID 1systemg:服务单元、依赖排序、SIGCHLD 回收与重启。
用户态splibc(freestanding C 库)、sh、systemg。动态链接的 BusyBox 通过 musl 运行。
终端ash,带作业控制:raw termios、poll、控制终端、前台进程组、Ctrl-C / Ctrl-Z。
x86_64已验证QEMU / UEFI 启动、systemg、BusyBox 动态加载、ash 交互和内核自测。
aarch64 / riscv64仅编译检查用户态和启动链路未验证。
真实硬件未验证只在 QEMU 的 VirtIO GPU 2D 上测过。
-
-
-
-
-
-

这一版新增的部分

-
    -
  • VFS 缓存

    Linux 语义的 dentry / inode 缓存:正、负 dentry,inode 身份,LRU 回收,路径前缀失效。procfs、sysfs、devtmpfs 每次都重新验证,避免设备注销后还查到旧目录。FAT32 的大文件读取另外做了簇链缓存。

  • -
  • 动态链接与 BusyBox

    内核装载 ELF 解释器和 PIE。镜像里有 musl 动态加载器和 BusyBox standalone shell;ash 能接收键盘和串口输入,并执行 applet。

  • -
  • 进程、信号与终端

    clone 线程、进程组与会话、wait4 / waitid、POSIX 信号、futex、epoll、signalfd、timerfd、AF_UNIX。终端支持控制终端、前后台进程组和 raw 模式。

  • -
  • 从 PID 1 到 shell

    systemg 作为 PID 1,解析 /etc/systemg/*.service,按依赖启动服务、回收孤儿进程,并拉起控制台 shell。splibc 提供静态 C 运行环境和自带的 sh。

  • -
-
-
- -
-
-

验证情况

-
- - - - - - - - - -
范围状态内容
x86_64已验证UEFI/QEMU 启动、systemg 作 PID 1、BusyBox 动态加载、ash 交互、uname / id / nproc / ls / cat、VFS dcache 自测、SELFTEST_OK,以及三个架构的 cargo check。
aarch64 / riscv64仅编译检查内核能通过编译检查。用户态、rootfs 和启动链路没有在这个镜像里验证。
真实硬件与 GPU未验证目前只在 QEMU 的 VirtIO GPU 2D 上验证。真实 GPU、音频、网络、睡眠唤醒不在这个镜像的承诺范围内。
-
-
-
- -
-
-

源码与许可

-
-

SpaceOS 9 的源码在 ViudiraTech/SpaceOS-9。镜像里包含 BusyBox、musl、Limine 等第三方组件,各自遵循自己的许可证,许可与源码获取方式见下载页和 THIRD_PARTY_NOTICES.md。

- -
+

源码与许可

+

源码在 ViudiraTech/SpaceOS-9。镜像里的 BusyBox、musl、Limine 等组件各有许可,见 THIRD_PARTY_NOTICES.md。

+

下载 SpaceOS 9 · 上一代:SpaceOS 8

+
@@ -245,7 +215,7 @@
- + From 62e594718ab7801d6da13af3c099f4ee4cf29935 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 02:29:02 +0000 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20=E9=85=8D=E7=BD=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E9=A1=B5=E6=94=B9=E4=B8=BA=E6=9C=80=E4=BD=8E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20/=20=E6=8E=A8=E8=8D=90=E9=85=8D=E7=BD=AE=E5=AF=B9?= =?UTF-8?q?=E7=85=A7=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 每个产品一张「项目 | 最低配置 | 推荐配置」表,下面只留一两句注意事项; 表里只用原页面已有的事实,没有单独推荐值的写「同左」 - 页面换成和介绍页一致的文档排版(左侧目录、单栏正文) - 对照表加 data-keep-labels:手机上最低 / 推荐两列都保留列名,并排成两栏 - 手机上配置要求页从约 6 屏缩到 4.9 屏 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SdLhesahBsuQSbGntwoQFN --- docs/css/style.css | 4 ++ docs/index.html | 4 +- docs/js/main.js | 2 +- docs/ota/index.html | 4 +- docs/ota/releases.html | 4 +- docs/pyspos.html | 4 +- docs/requirements.html | 137 +++++++++++++++++------------------- docs/spaceos6.html | 4 +- docs/spaceos7.html | 4 +- docs/spaceos8-download.html | 4 +- docs/spaceos8.html | 4 +- docs/spaceos9-download.html | 4 +- docs/spaceos9.html | 4 +- 13 files changed, 88 insertions(+), 95 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index 79bb35b..7704670 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1529,6 +1529,10 @@ html.has-toc { scroll-padding-top: calc(var(--pg-header-h) + 3.5rem); } margin-top: 0; } .table-wrap td:has(> .badge:only-child)::before { display: none; } + /* 最低 / 推荐对照表:卡片里两栏并排,一眼对比 */ + .table-wrap table[data-keep-labels] tbody tr { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .table-wrap table[data-keep-labels] tbody tr > th { grid-column: 1 / -1; } + .table-wrap table[data-keep-labels] tbody tr > td { grid-column: auto; grid-row: 2; margin-top: var(--pg-space-2); } /* 数字列(行数等)同样放到标题右边,条形图缩短后跟在数字上方 */ .table-wrap tbody tr > td.num { grid-column: 2; diff --git a/docs/index.html b/docs/index.html index 3434fb0..33ef26f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/js/main.js b/docs/js/main.js index c44d911..48c0e0f 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -145,7 +145,7 @@ totals[i] = (totals[i] || 0) + cell.textContent.trim().length; }); }); - var main = totals.indexOf(Math.max.apply(null, totals)); + var main = table.hasAttribute("data-keep-labels") ? -1 : totals.indexOf(Math.max.apply(null, totals)); rows.forEach(function (tr) { Array.prototype.forEach.call(tr.children, function (cell, i) { var label = heads[i] && heads[i].textContent.trim(); diff --git a/docs/ota/index.html b/docs/ota/index.html index 30ab09e..7e0f5ea 100644 --- a/docs/ota/index.html +++ b/docs/ota/index.html @@ -7,7 +7,7 @@ PySpOS 更新包 | GoutouStdio - + @@ -224,7 +224,7 @@ - + diff --git a/docs/ota/releases.html b/docs/ota/releases.html index ef4169a..a5f082c 100644 --- a/docs/ota/releases.html +++ b/docs/ota/releases.html @@ -7,7 +7,7 @@ PySpOS 版本历史 | GoutouStdio - + @@ -154,7 +154,7 @@ - + diff --git a/docs/pyspos.html b/docs/pyspos.html index 5ad99c8..ef29ef7 100644 --- a/docs/pyspos.html +++ b/docs/pyspos.html @@ -7,7 +7,7 @@ PySpOS | GoutouStdio - + @@ -233,7 +233,7 @@ - + diff --git a/docs/requirements.html b/docs/requirements.html index b29a086..063b365 100644 --- a/docs/requirements.html +++ b/docs/requirements.html @@ -7,7 +7,7 @@ 配置要求 | GoutouStdio - + @@ -79,100 +79,89 @@

配置要求

-

PySpOS 是跑在宿主系统上的 Python 程序;SpaceOS 8 和 9 是要自己启动的内核。条件差别大,所以按产品分开写。

+

按产品列出最低配置和推荐配置。最低配置能运行,推荐配置体验更好。

-
-
-

PySpOS 3.2.0

-
-

纯用户态,不直接操作硬件,能跑 Python 的机器就能跑。

-
- +
+
+ +
+

PySpOS 3.2.0

+

纯用户态程序,能跑 Python 就能跑。

+
+
+ + + - - - - - - + + + + + + +
项目最低配置推荐配置
Python3.10 或更高。CI 覆盖 3.10、3.12、3.14。
系统Windows 7 及以上、Linux 各发行版、macOS。
磁盘约 120 MB。不需要管理员权限。
CPU / 内存双核 CPU、512 MB 内存足够,不需要独立显卡。
终端curses 的首次开机向导需要 80×24 字符的终端。SSH、tmux、Windows Terminal 都可以。
可选依赖unicorn(ELF 引擎,缺失时退回自研模拟器);tzdata(真实时区,Windows 和精简容器镜像需要)。都不是硬性要求。
Python3.103.12 或 3.14(CI 测过的版本)
系统Windows 7、主流 Linux、macOS 10.14同左
处理器双核同左
内存512 MB同左
存储120 MB 可用空间同左
终端80×24 字符同左
可选依赖无unicorn(ELF 引擎)、tzdata(真实时区)
-

PySpOS 介绍 · 下载更新包

-
-
-
+

不需要管理员权限和独立显卡。缺 unicorn 时退回自研模拟器,Windows 和精简容器镜像需要 tzdata 才有真实时区。

-
-
-

SpaceOS 9 公开预览

-
+

SpaceOS 9 公开预览

目前只在 QEMU 上验证过。

-
- +
+
+ + + - - - - - + + + +
项目最低配置推荐配置
虚拟机x86_64 QEMU,推荐 Q35 机型和 VirtIO GPU 2D。
固件OVMF UEFI 固件。
内存至少 512 MiB。
未验证真实硬件、真实 GPU、音频、网络、睡眠唤醒。
其他架构aarch64 / riscv64 只通过编译检查,镜像里没有它们的用户态。
平台x86_64 QEMUx86_64 QEMU,Q35 机型
固件OVMF(UEFI)同左
内存512 MiB同左
显示—VirtIO GPU 2D
-

SpaceOS 9 介绍 · QEMU 启动命令

-
-
-
+

真实硬件、GPU、音频、网络和睡眠唤醒都没有验证。aarch64 / riscv64 只通过编译检查,镜像里没有它们的用户态。启动命令见下载页。

-
-
-

SpaceOS 8

-
-
- -
-

请用可抛弃的机器和测试盘

-

NTFS 写入驱动目前还不能信任,确认稳定前不要让它碰重要数据。已在 Dell PowerEdge R410 等实体服务器上验证。

- -
+

SpaceOS 8

+
+ +
+

请用可抛弃的机器和测试盘

+

NTFS 写入驱动还不能信任,不要让它碰重要数据。

-

表里“已验证”是实际跑过的组合,“默认关闭”的需要自己在 make menuconfig 里打开。

-
- + +
+
- + - - - - - - - - + + + + + + + +
硬件要求状态
项目最低配置推荐配置
处理器x86_64 必需。多核可用,SMP 启动会拉起全部核心,并在核间迁移任务、均衡负载。已验证
内存最低 128 MB,512 MB 可启动。跑图形桌面并使用 swap,建议 1 GB 以上。
固件经 Limine 引导,UEFI 与 Legacy 两种模式都支持,不挑固件实现。UEFI 已验证
输入PS/2 键盘和鼠标是主路径,提供 Linux 兼容的 evdev;另支持 USB HID(键盘、鼠标、消费类控制器)。PS/2 路径仍在验证中。xHCI USB HID 已验证
显示DRM/KMS 内核核心、通用 GPU 驱动注册表、内置 VirtIO-GPU 驱动;GOP 帧缓冲控制台(点阵字体)和软件帧缓冲兜底。没接显卡时可用 GOP 文本控制台。真实 GPU 驱动未完成
存储IDE/ATA、AHCI(SATA)、NVMe、USB Mass Storage 都有驱动(NVMe 已启用)。默认根文件系统是 tmpfs。NTFS 写入不建议用
网络需要 Intel e1000 / e1000e 或 Realtek RTL8139 / RTL8169。协议栈自研,客户端带 DHCP。其他网卡可能无法联网。需要指定网卡
音频(可选)Sound Blaster 16 与 Intel HD Audio,带 ALSA 兼容的 PCM / control ABI。SB16 需要在 menuconfig 里打开。SB16 默认关闭
处理器x86_64x86_64 多核(支持 SMP)
内存128 MB1 GB 以上(图形桌面 + swap)
固件UEFI 或 Legacy BIOSUEFI(已验证)
显示GOP 帧缓冲VirtIO-GPU;真实显卡驱动未完成
存储不需要(默认根是 tmpfs)IDE / AHCI / NVMe / USB 存储
输入PS/2 键盘和鼠标USB 键盘和鼠标(xHCI,已验证)
网络可以不接Intel e1000 / e1000e 或 Realtek RTL8139 / RTL8169
音频可以没有Intel HD Audio
-

构建

-

必需:make、gcc(建议 13.3+)、qemu、xorriso。建议:clang-format、clang-tidy。只有 menuconfig 需要 kconfig-frontends 和 libncurses-dev。

-
sudo apt install build-essential qemu-system-x86 xorriso libncurses-dev
-

默认关闭的功能

-

有些子系统在 Kconfig 里默认是 n,要用得自己打开:VIRTIO / VIRTIO_GPU(虚拟化设备)、SOUND_SB16(声卡)。用 make menuconfig 打开,并确认虚拟机或实体机上有对应设备。新增 GPU 驱动,是在 drivers/gpu/gpu_drivers.c 的注册表里加一条记录。

-

SpaceOS 8 介绍 · 内核源码与构建说明

-
-
-
+

VirtIO 和 SB16 声卡默认关闭,要用 make menuconfig 打开。实体机已在 Dell PowerEdge R410 上验证。构建依赖见 SpaceOS 8 介绍。

-
-
-

SpaceOS 6 / 7

-
-

这两个项目已停止开发,这里不再列硬件要求。页面仅作历史存档,配置和构建说明以当时的仓库记录为准。

-

SpaceOS 7 存档 · SpaceOS 6 存档

-
+

SpaceOS 6 / 7

+

已停止开发,不再列配置要求。见 SpaceOS 7 和 SpaceOS 6 存档页。

+
@@ -223,7 +212,7 @@ - + diff --git a/docs/spaceos6.html b/docs/spaceos6.html index ce31c11..01ff5f2 100644 --- a/docs/spaceos6.html +++ b/docs/spaceos6.html @@ -7,7 +7,7 @@ SpaceOS 6(已停更) | GoutouStdio - + @@ -185,7 +185,7 @@ - + diff --git a/docs/spaceos7.html b/docs/spaceos7.html index ef2b59d..ba5a159 100644 --- a/docs/spaceos7.html +++ b/docs/spaceos7.html @@ -7,7 +7,7 @@ SpaceOS 7(已停更) | GoutouStdio - + @@ -182,7 +182,7 @@ - + diff --git a/docs/spaceos8-download.html b/docs/spaceos8-download.html index 0704cad..c156dfb 100644 --- a/docs/spaceos8-download.html +++ b/docs/spaceos8-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 8 | GoutouStdio - + @@ -239,7 +239,7 @@ - + diff --git a/docs/spaceos8.html b/docs/spaceos8.html index 0a8e1f4..7ce53fc 100644 --- a/docs/spaceos8.html +++ b/docs/spaceos8.html @@ -7,7 +7,7 @@ SpaceOS 8 | GoutouStdio - + @@ -272,7 +272,7 @@ - + diff --git a/docs/spaceos9-download.html b/docs/spaceos9-download.html index ce03ed4..533bbf6 100644 --- a/docs/spaceos9-download.html +++ b/docs/spaceos9-download.html @@ -7,7 +7,7 @@ 下载 SpaceOS 9 | GoutouStdio - + @@ -232,7 +232,7 @@ - + diff --git a/docs/spaceos9.html b/docs/spaceos9.html index 2cea06e..1c0da4b 100644 --- a/docs/spaceos9.html +++ b/docs/spaceos9.html @@ -7,7 +7,7 @@ SpaceOS 9 | GoutouStdio - + @@ -215,7 +215,7 @@ - +