fix(agc): 外壳滚动收敛到 .launcher-main 单一容器(含页面聚焦描边与继承徽章样式)
This commit is contained in:
@@ -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]');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user