fix(agc): 外壳滚动收敛到 .launcher-main 单一容器(含页面聚焦描边与继承徽章样式)

This commit is contained in:
2026-10-07 00:53:21 +08:00
parent 15871914f7
commit 501fd0925d
2 changed files with 184 additions and 14 deletions
+56 -14
View File
@@ -678,14 +678,6 @@ body {
overflow: auto;
}
.window-chrome__content .launcher-main:has(.game-project-workbench) {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding-bottom: 0;
}
.window-chrome__content .game-project-workbench {
flex: 1 1 auto;
height: auto;
@@ -1083,14 +1075,38 @@ textarea {
.launcher-main {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
align-content: start;
/* 页面滚动就落在这里:**所有** launcher 视图(首页 / 项目 / 共创 / 确认页…)都是
`.launcher-main` 的直接子元素,因此滚动容器全应用只有一个,页面不用各自打补丁。
之前是 `display: grid; grid-template-rows: auto auto auto; align-content: start`:
行高是 `auto`,子元素撑多高行就多高,再叠加 `overflow: hidden` —— 内容一旦高于窗口就被
整块裁掉,既看不到也滚不到(截图里的「支持共创的作品」最后一行、确认页底部按钮够不到
都是这个原因)。改成 flex 列 + 纵向滚动:子元素保持自上而下排列,超出即滚。 */
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
min-height: 100vh;
min-height: 0;
padding: 0 0 42px;
overflow: hidden;
/* 纵向滚动 / 横向裁切:横向裁切必须保留,否则窄窗口下会被内容顶出横向滚动条。 */
overflow-x: hidden;
overflow-y: auto;
}
/*
视图本身是 `.launcher-main` 的 flex 子项:默认 `flex-shrink: 1` 会把「比窗口高的页面」
压扁到容器高度(内容溢出而不触发滚动)。这里统一 `flex: 0 0 auto`,让页面保持自然高度、
由 `.launcher-main` 滚动。需要占满高度的视图(工作台)在自己的规则里改回 `flex: 1 1 auto`。
*/
.launcher-main > * {
flex: 0 0 auto;
}
/* 共创相关页面的键盘聚焦:与 `.launcher-projects-page` 同一套描边口径。 */
.fork-page
:where(button, a, input, textarea, [tabindex]):focus-visible {
outline: 3px solid var(--platform-warm-text);
outline-offset: 2px;
}
.launcher-runs-panel {
@@ -3702,6 +3718,32 @@ textarea {
line-height: 1.5;
}
/* 衍生作品的只读授权:徽章(档位)+ 说明两行,形态上明显不是可选控件。 */
.game-distribution-publish-panel__fork-inherited {
display: grid;
gap: 6px;
margin: 0;
color: var(--platform-text-muted);
font-size: 11px;
font-weight: 500;
line-height: 1.5;
}
.game-distribution-publish-panel__fork-inherited-badge {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 6px;
padding: 2px 10px;
border: 1px solid var(--platform-warm-border);
border-radius: 999px;
background: var(--platform-warm-bg);
color: var(--platform-warm-text);
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* 工程源码公开勾选:与上面的三态同宽同位,复选框不能套用「整行输入框」样式。 */
.game-distribution-publish-panel__fields
.game-distribution-publish-panel__bundle-consent {
@@ -0,0 +1,128 @@
/**
* 外壳滚动链的**契约测试**(纯 CSS 断言,不需要浏览器)。
*
* 背景:`.launcher-main` 曾经是 `display: grid` + `grid-template-rows: auto …` + `overflow: hidden`,
* 视图一旦比窗口高就被整块裁掉——「共创」列表最后一行、Fork 确认页底部操作条都够不到。
* 修法是把 `.launcher-main` 变成**唯一的页面滚动容器**(flex 列 + `overflow-y: auto`),
* 视图保持自然高度(`flex: 0 0 auto`)。
*
* jsdom 没有布局,量不出 `scrollHeight > clientHeight`;这里钉住的是**样式契约**(哪一层负责
* 滚动、链路上的每一层都允许收缩),真实滚动行为由浏览器冒烟核对。
*/
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { repoPath } from './repoPath';
const stylesPath = repoPath('apps/ai-game-creator-shell/src/styles.css');
const styles = readFileSync(stylesPath, 'utf8');
/** 取某个选择器的第一条声明块(与 `workbenchThemeContrast.test.ts` 同一套解析口径)。 */
function getCssBlock(source: string, selector: string) {
const selectorIndex = source.indexOf(selector);
expect(selectorIndex, `${selector} should exist`).toBeGreaterThanOrEqual(0);
const openBraceIndex = source.indexOf('{', selectorIndex);
let depth = 0;
for (let index = openBraceIndex; index < source.length; index += 1) {
const char = source[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(openBraceIndex + 1, index);
}
}
}
throw new Error(`${selector} block is not closed`);
}
/** 去掉注释再断言:注释里会引用旧写法(例如「以前是 grid-template-rows…」)。 */
function stripComments(block: string) {
return block.replace(/\/\*[\s\S]*?\*\//gu, '');
}
function declaration(block: string, property: string) {
const match = block.match(new RegExp(`(^|[;{\\s])${property}:\\s*([^;]+);`));
expect(match, `${property} should be declared`).not.toBeNull();
return (match?.[2] ?? '').replace(/\s+/gu, ' ').trim();
}
describe('launcher 滚动链', () => {
it('根与外壳层都给足高度,允许收缩', () => {
const root = getCssBlock(styles, '#root {');
expect(declaration(root, 'height')).toBe('100dvh');
expect(declaration(root, 'min-height')).toBe('0');
const chromeContent = getCssBlock(styles, '.window-chrome__content {');
expect(declaration(chromeContent, 'height')).toBe('100%');
expect(declaration(chromeContent, 'min-height')).toBe('0');
// 外壳内容区自己不能滚:滚动只发生在 `.launcher-main`。
expect(declaration(chromeContent, 'overflow')).toBe('hidden');
const launcherShell = getCssBlock(
styles,
'.window-chrome__content > .launcher-shell',
);
expect(declaration(launcherShell, 'height')).toBe('100%');
expect(declaration(launcherShell, 'min-height')).toBe('0');
});
it('`.launcher-main` 是唯一页面滚动容器(flex 列 + 纵向 auto)', () => {
const main = getCssBlock(styles, '\n.launcher-main {');
expect(declaration(main, 'display')).toBe('flex');
expect(declaration(main, 'flex-direction')).toBe('column');
// 纵向滚动 + 横向裁切:窄窗口不会被内容顶出横向滚动条。
expect(declaration(main, 'overflow-y')).toBe('auto');
expect(declaration(main, 'overflow-x')).toBe('hidden');
// 不允许再出现把内容整块裁掉的 `overflow: hidden`(横向那条是显式 overflow-x)。
expect(main).not.toMatch(/(^|[;\s])overflow:\s*hidden;/u);
const mainOverride = getCssBlock(
styles,
'.window-chrome__content > .launcher-shell .launcher-main {',
);
expect(declaration(mainOverride, 'height')).toBe('100%');
expect(declaration(mainOverride, 'min-height')).toBe('0');
// 视图保持自然高度:`flex-shrink: 1` 会把长页面压扁到容器高度。
const viewItem = getCssBlock(styles, '.launcher-main > * {');
expect(declaration(viewItem, 'flex')).toBe('0 0 auto');
});
it('旧的 grid 行高与裁切不再决定滚动', () => {
const main = stripComments(getCssBlock(styles, '\n.launcher-main {'));
expect(main).not.toMatch(/grid-template-rows/u);
expect(main).not.toMatch(/grid-template-columns/u);
expect(main).not.toMatch(/align-content/u);
// 工作台仍可特判自己占满高度,但不得再改滚动方式(滚动只有一处)。
const workbenchRule = getCssBlock(
styles,
'.launcher-main:has(.game-project-workbench) {',
);
expect(workbenchRule).not.toMatch(/overflow|display|height/u);
});
it('共创相关页面共享一套版式令牌,且正文不吃满整屏', () => {
const tokens = readFileSync(
repoPath(
'apps/ai-game-creator-shell/src/features/platform-fork/forkPageStyles.ts',
),
'utf8',
);
const column = tokens.match(
/export const FORK_PAGE_COLUMN =\s*'[^']*min\((\d+)px/u,
);
expect(
column,
'FORK_PAGE_COLUMN should cap the column width',
).not.toBeNull();
const width = Number(column?.[1]);
expect(width).toBeGreaterThanOrEqual(720);
expect(width).toBeLessThanOrEqual(880);
// 正文另有 72ch 上限:长文不铺满整个内容列。
expect(tokens).toContain('max-w-[72ch]');
});
});