review: address ds-review-bot findings on composer geometry
- Rewrite the 08-04 gutter-reservation note in place: the reservation is Chat's alone and the overlay branch points to the seat-width compensation note; the Testing section now describes the uncompensated control. - Cross-reference the earlier rejection of the seat inset in the 08-12 note and record the engine-resolved-vs-fixed-8px drift as accepted residual cost instead of asserting Firefox resolves 8px. - Pair --dsh-scrollbar-width with the mirrored ::-webkit-scrollbar rule and the compensation consumer in the scrollbar-styles gate; document the variable in the ui-theme README. - Rename the e2e control to compareTabsWithoutCompensation and correct the vacuity-guard comment; rewrap the WorkspaceBrowser doc comment.
This commit is contained in:
@@ -297,8 +297,10 @@
|
||||
by the bar's width; the overlay seat is against the padding box, so it
|
||||
must give back the same width to keep the input card in place across
|
||||
tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit
|
||||
bar's layout width; Firefox's thin bar resolves to the same 8px on the
|
||||
platforms this app targets, so the compensation holds there too. */
|
||||
bar's layout width. Chat reserves the engine-resolved width, so on an
|
||||
engine where the resolved width differs from this fixed 8px the card
|
||||
drifts by half the difference — accepted residual risk, recorded in
|
||||
([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md)). */
|
||||
right: var(--dsh-scrollbar-width);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md
|
||||
README.md: df4d5e0370962bf6f2a8ac0a7b88d669225dc5c5
|
||||
README.zh.md: fb8e937979a5168803be78a49e1bc1e7eacd7a47
|
||||
README.md: c24f2ae5692fdd05134ce780d21569b87570d1c7
|
||||
README.zh.md: 518b8930027c88bd85139a8a7325cea1fd40b40c
|
||||
|
||||
@@ -8,7 +8,7 @@ When the host composition includes an HTTP server, the host half injects a synch
|
||||
|
||||
`src/styles/` holds five sheets, all imported by the web shell's `base.css`: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.
|
||||
|
||||
Scrollbar rebinding contract: `scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 (base-surface) tokens, and both rendering paths read that pair. An elevated surface (menu, popover, dialog) sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container; one rebind retints whichever path the engine took. The pair's other legal target is `transparent`, which draws no thumb at all — [ui-sidebar](../ui-sidebar/README.md) rebinds its column that way while the pointer is elsewhere. A rebind to the l1 pair is not a rebind; it restates the base-surface default.
|
||||
Scrollbar rebinding contract: `scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 (base-surface) tokens, and both rendering paths read that pair. An elevated surface (menu, popover, dialog) sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container; one rebind retints whichever path the engine took. The pair's other legal target is `transparent`, which draws no thumb at all — [ui-sidebar](../ui-sidebar/README.md) rebinds its column that way while the pointer is elsewhere. A rebind to the l1 pair is not a rebind; it restates the base-surface default. `--dsh-scrollbar-width` mirrors the WebKit bar's layout width for surfaces that align themselves beside a space-consuming bar — [ui-conversation](../ui-conversation/README.md) reads it for the overlay composer seat's `right` offset — and the scrollbar-styles spec pairs it with the mirrored rule and the consumer.
|
||||
|
||||
The two paths are mutually exclusive by construction. `scrollbar-width`/`scrollbar-color` sit inside `@supports not selector(::-webkit-scrollbar)` because a non-`auto` value of either makes Chromium and Safari discard every `::-webkit-scrollbar*` rule for that element, `::-webkit-scrollbar-thumb:hover` included — declaring both unconditionally leaves `--dsh-scrollbar-thumb-hover` with no rendering anywhere. Firefox therefore takes the standard properties and WebKit-based engines take the pseudo-elements, so the hover token only ever renders through the pseudo-element path. Reasoning and the measured computed values: [the scrollbar Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md).
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
`src/styles/` 下有五张样式表,全部由 web 壳的 `base.css` 导入:`base.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。
|
||||
|
||||
滚动条重新绑定约定:`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1(基础表面)token,两条渲染路径都读取这一组变量。高层级表面(菜单、浮层、对话框)在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`;一次重新绑定即可为引擎实际走的那条路径换色。这组变量的另一个合法目标是 `transparent`,即完全不绘制滑块——[ui-sidebar](../ui-sidebar/README.md) 在指针不在栏内时就这样重新绑定自己的列。绑回 l1 那组不算重新绑定,它只是重述基础表面的默认值。
|
||||
滚动条重新绑定约定:`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1(基础表面)token,两条渲染路径都读取这一组变量。高层级表面(菜单、浮层、对话框)在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`;一次重新绑定即可为引擎实际走的那条路径换色。这组变量的另一个合法目标是 `transparent`,即完全不绘制滑块——[ui-sidebar](../ui-sidebar/README.md) 在指针不在栏内时就这样重新绑定自己的列。绑回 l1 那组不算重新绑定,它只是重述基础表面的默认值。`--dsh-scrollbar-width` 镜像 WebKit 滚动条的布局宽度,供需要与占布局宽度的滚动条对齐的表面使用——[ui-conversation](../ui-conversation/README.md) 用它作为覆盖 composer 座位 `right` 偏移——scrollbar-styles 规格把它与镜像规则及消费者配对检查。
|
||||
|
||||
两条路径在构造上互斥。`scrollbar-width`/`scrollbar-color` 写在 `@supports not selector(::-webkit-scrollbar)` 之内,因为这两个属性中的任一个只要取非 `auto` 值,Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中——若无条件地同时声明,`--dsh-scrollbar-thumb-hover` 在任何引擎上都不会被渲染。因此 Firefox 走标准属性,WebKit 系引擎走伪元素,hover token 只经由伪元素这条路径渲染。相关原理与实测计算值见[滚动条 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)。
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* Scrollbar stylesheet contract, asserted against the CSS text on disk: every
|
||||
* --dsw-alias-scrollbar-* token design-platform.css defines has a consumer,
|
||||
* scrollbar.css binds the base-surface pair through the rebindable
|
||||
* indirection, and elevated surfaces rebind that indirection in complete
|
||||
* pairs. The expected token set is scanned out of design-platform.css, so
|
||||
* adding, renaming, or dropping a scrollbar token moves these assertions with
|
||||
* it.
|
||||
* indirection, the width variable mirrors the ::-webkit-scrollbar rule for
|
||||
* consumers that align beside the bar, and elevated surfaces rebind that
|
||||
* indirection in complete pairs. The expected token set is scanned out of
|
||||
* design-platform.css, so adding, renaming, or dropping a scrollbar token
|
||||
* moves these assertions with it.
|
||||
*/
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
@@ -341,6 +342,67 @@ describe('scrollbar.css base-surface binding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('scrollbar.css width variable', () => {
|
||||
const WIDTH_VARIABLE = `${INDIRECTION_PREFIX}width`
|
||||
|
||||
it('defines the width variable on body as a static length', () => {
|
||||
// The overlay seat compensation reads a fixed number, not a second
|
||||
// indirection: the mirror check below compares the WebKit rule against
|
||||
// this value, so a var()-to-var() chain would compare one indirection to
|
||||
// another instead of pinning the number.
|
||||
const value = scrollbarRules
|
||||
.filter(rule => rule.selectors.includes('body'))
|
||||
.flatMap(rule => rule.declarations)
|
||||
.findLast(([property]) => property === WIDTH_VARIABLE)?.[1]
|
||||
expect(value, WIDTH_VARIABLE).toBeDefined()
|
||||
expect(value, WIDTH_VARIABLE).toMatch(/^\d+(?:\.\d+)?px$/)
|
||||
})
|
||||
|
||||
it('mirrors the ::-webkit-scrollbar width rule with the variable value', () => {
|
||||
// The compensation stays aligned with the WebKit bar only while both read
|
||||
// the same number. A change to one side without the other puts the overlay
|
||||
// seat a band off from Chat on WebKit engines.
|
||||
const variableValue = scrollbarRules
|
||||
.filter(rule => rule.selectors.includes('body'))
|
||||
.flatMap(rule => rule.declarations)
|
||||
.findLast(([property]) => property === WIDTH_VARIABLE)?.[1]
|
||||
const webkitWidth = scrollbarRules
|
||||
.filter(rule => rule.selectors.includes('::-webkit-scrollbar'))
|
||||
.flatMap(rule => rule.declarations)
|
||||
.findLast(([property]) => property === 'width')?.[1]
|
||||
expect(webkitWidth, '::-webkit-scrollbar width').toBeDefined()
|
||||
expect(webkitWidth).toBe(variableValue)
|
||||
})
|
||||
|
||||
it('every reader of the width variable outside ui-theme references a defined variable', () => {
|
||||
// The consumer is ConversationRoot's overlay composer seat
|
||||
// (`right: var(--dsh-scrollbar-width)`); a rename in scrollbar.css without
|
||||
// the consumer, or a typo in the consumer, leaves the value
|
||||
// guaranteed-invalid and the seat loses the band. The equal-rectangle e2e
|
||||
// would catch it only on an engine that draws the bar, so the sheet
|
||||
// contract states it here.
|
||||
const defined = new Set(
|
||||
scrollbarRules
|
||||
.flatMap(rule => rule.declarations)
|
||||
.filter(([property]) => property.startsWith(INDIRECTION_PREFIX))
|
||||
.map(([property]) => property),
|
||||
)
|
||||
expect(defined).toContain(WIDTH_VARIABLE)
|
||||
const readers: string[] = []
|
||||
for (const file of packageStylesheets()) {
|
||||
if (file === fileURLToPath(new URL('scrollbar.css', STYLES))) continue
|
||||
for (const rule of parseRules(readFileSync(file, 'utf8'))) {
|
||||
for (const [property, value] of rule.declarations) {
|
||||
for (const name of varReferences(value)) {
|
||||
if (name === WIDTH_VARIABLE) readers.push(`${file} ${rule.selectors.join(', ')}: ${property}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(readers.length, 'compensation consumer').toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('scrollbar.css selectors', () => {
|
||||
const scrollbarColorSelectors = scrollbarRules
|
||||
.filter(rule => rule.declarations.some(([property]) => property === 'scrollbar-color'))
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
* workspace), search, the grouped tree or flat list, and the workspace
|
||||
* dialogs. Wide state renders the full browser; rail state renders the two
|
||||
* region icons (search / add workspace) as 36px controls on the shell's shared
|
||||
* rail entry path, each requesting expansion through the owner share. Adding is the header
|
||||
* button's one action, so it raises the directory flow with no menu in
|
||||
* between; the flow and its error dialog live in WorkspacePicker (same
|
||||
* package — direct composition, no slot between them).
|
||||
* rail entry path, each requesting expansion through the owner share. Adding
|
||||
* is the header button's one action, so it raises the directory flow with no
|
||||
* menu in between; the flow and its error dialog live in WorkspacePicker
|
||||
* (same package — direct composition, no slot between them).
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
Reference in New Issue
Block a user