fix(ui): hide sidebar resize pill
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 .agents/notes/implemented/simplification/2026-07-30-sidebar-resize-without-visible-pill.md
|
||||
2026-07-30-sidebar-resize-without-visible-pill.md: cc41898990fa23ff2937140186a8324217911d2e
|
||||
2026-07-30-sidebar-resize-without-visible-pill.zh.md: 9f1f521df2848b15f5015719bfa6f0e0e9b7be0c
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Sidebar resize without a visible pill
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-30-sidebar-resize-without-visible-pill.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The AppFrame exposed identical floating pills on both column borders. The left pill added unnecessary visual weight beside primary navigation, but the sidebar's resize interaction remains useful.
|
||||
|
||||
## Decision
|
||||
|
||||
AppFrame keeps the sidebar's 8px resize hit strip, `col-resize` cursor, pointer capture, animation-frame throttling, and width updates, but does not generate the sidebar handle's pill pseudo-element. The details boundary retains both its hit strip and floating pill.
|
||||
|
||||
The layout component test continues to pin sidebar dragging and both handles' collapse lifecycle. A keyless browser scenario reads the generated pseudo-elements from the shipped composition and drags the invisible sidebar boundary to prove the interaction remains live.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Remove the sidebar drag interaction with the pill.** Rejected because the requested change is visual; removing a working geometry control would unnecessarily narrow the interaction.
|
||||
|
||||
**Keep the pill but reduce its emphasis.** A smaller or lower-contrast pill still leaves an unwanted object on the sidebar boundary.
|
||||
|
||||
## Consequences
|
||||
|
||||
The sidebar boundary is visually quiet while pointer resizing remains available from the boundary and retains the resize cursor. Unlike the details control, that interaction has no visible pill.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 侧边栏缩放不显示胶囊
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-30-sidebar-resize-without-visible-pill.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
AppFrame 在两个栏位边界都显示相同的浮动胶囊。左侧胶囊在主导航旁增加了不必要的视觉负担,但侧边栏的缩放交互仍有用。
|
||||
|
||||
## 决策
|
||||
|
||||
AppFrame 保留侧边栏宽 8px 的缩放命中条带、`col-resize` 光标、指针捕获、动画帧节流和宽度更新,但不再生成侧边栏手柄的胶囊形伪元素。详情栏边界同时保留命中条带和浮动胶囊。
|
||||
|
||||
布局组件测试继续固定侧边栏拖动行为,以及两个手柄随面板折叠时的生命周期。一个无密钥浏览器场景读取实际交付组合所生成的伪元素,并拖动不可见的侧边栏边界,证明该交互仍然有效。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**随胶囊一并移除侧边栏拖动交互。** 不予采纳,因为本次要求只改视觉表现;移除正常工作的几何控制会不必要地缩减交互方式。
|
||||
|
||||
**保留胶囊,但降低其视觉强调。** 更小或对比度更低的胶囊仍会在侧边栏边界留下一个不需要的物体。
|
||||
|
||||
## 后果
|
||||
|
||||
侧边栏边界在视觉上保持简洁,同时仍可在边界处通过指针调整宽度,并保留缩放光标。与详情栏控件不同,该交互没有可见胶囊。
|
||||
@@ -2,14 +2,18 @@
|
||||
// The shipped composition retains geometry through unselected states and closes it only when a different Session takes ownership.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
assertFixtureInventory, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, seedSession,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/details-session-lifecycle', import.meta.url))
|
||||
const HANDLES_EXPECTED = join(SNAPSHOT_DIR, 'handles.expected.md')
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/lifecycle-chrome/session.jsonl', import.meta.url))
|
||||
const SEED_FIXTURE = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
|
||||
@@ -23,11 +27,41 @@ async function detailsTrack(page: Page): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
/** First AppFrame grid track in CSS pixels. */
|
||||
async function sidebarTrack(page: Page): Promise<number> {
|
||||
return await appFrame(page).evaluate((element) => {
|
||||
const tracks = getComputedStyle(element).gridTemplateColumns.split(' ')
|
||||
return Number.parseFloat(tracks[0] ?? 'NaN')
|
||||
})
|
||||
}
|
||||
|
||||
/** AppFrame is the only product element with an inline grid track template. */
|
||||
function appFrame(page: Page) {
|
||||
return page.locator('[style*="grid-template-columns"]').first()
|
||||
}
|
||||
|
||||
/** Render the two boundary affordances without platform-dependent coordinates. */
|
||||
async function handleSnapshot(page: Page): Promise<string> {
|
||||
const handles = await page.locator('[class*="handle"]').evaluateAll(elements =>
|
||||
elements.map(element => ({
|
||||
side: element.getAttribute('data-side'),
|
||||
cursor: getComputedStyle(element).cursor,
|
||||
pillGenerated: getComputedStyle(element, '::after').content !== 'none',
|
||||
})))
|
||||
return [
|
||||
'# AppFrame drag handles',
|
||||
'',
|
||||
...handles.flatMap(handle => [
|
||||
`## ${handle.side}`,
|
||||
'',
|
||||
'- hit strip present: true',
|
||||
`- cursor: ${handle.cursor}`,
|
||||
`- pill generated: ${String(handle.pillGenerated)}`,
|
||||
'',
|
||||
]),
|
||||
].join('\n').trimEnd()
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: details panel follows the current Session lifecycle', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
@@ -63,6 +97,18 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
|
||||
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
|
||||
await compareOrRefreshGolden(HANDLES_EXPECTED, await handleSnapshot(page), MODE)
|
||||
|
||||
const sidebarBefore = await sidebarTrack(page)
|
||||
const sidebarHandle = page.locator('[data-side="sidebar"]')
|
||||
const sidebarBox = await sidebarHandle.boundingBox()
|
||||
expect(sidebarBox).not.toBeNull()
|
||||
const dragStartX = sidebarBox!.x + sidebarBox!.width / 2
|
||||
await page.mouse.move(dragStartX, sidebarBox!.y + 200)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(dragStartX + 70, sidebarBox!.y + 200, { steps: 6 })
|
||||
await page.mouse.up()
|
||||
await expect.poll(() => sidebarTrack(page), { timeout: 5_000 }).toBe(sidebarBefore + 70)
|
||||
|
||||
await page.getByRole('button', { name: 'New session', exact: true }).last().click()
|
||||
await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
|
||||
@@ -91,5 +137,6 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md'])
|
||||
}, 90_000)
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
# AppFrame drag handles
|
||||
|
||||
## sidebar
|
||||
|
||||
- hit strip present: true
|
||||
- cursor: col-resize
|
||||
- pill generated: false
|
||||
|
||||
## details
|
||||
|
||||
- hit strip present: true
|
||||
- cursor: col-resize
|
||||
- pill generated: true
|
||||
@@ -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-layout/README.md
|
||||
README.md: 9354f4b79f7b1af7d8a20a295e77913ff443c2e4
|
||||
README.zh.md: c949236557e7eb3eed0c698566fb5aa9e9cdd18a
|
||||
README.md: 250828764ecb2c620410fc59d84076f4117d7fc4
|
||||
README.zh.md: 17c45794ff7ce7ef90e7a233abcef82321a87407
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar is fixed-width (only details shrinks, then auto-closes); a closed sidebar retains a 56px control rail while details closes to zero width. The package also seats the theme presenter: it consumes resolved `ctx.theme` snapshots and projects them onto the document (`html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, plus the theme's alias tokens as inline variables on body).
|
||||
Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar resize boundary is an invisible hit strip, while the details boundary retains its floating pill; only details shrinks during concession and then auto-closes. A closed sidebar retains a 56px control rail while details closes to zero width. The package also seats the theme presenter: it consumes resolved `ctx.theme` snapshots and projects them onto the document (`html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, plus the theme's alias tokens as inline variables on body).
|
||||
|
||||
AppFrame always mounts the conversation and details columns; a connected Session renders through `SessionProvider`. The transient layout store starts both panels at their default widths and never reads or writes `localStorage`. Hero and other unselected states derive a zero rendered details width without changing that stored preference. AppFrame retains the last non-blank Session id across those states: the first Session opens at the default width, returning to the same Session restores its unchanged width, and selecting a different Session closes details before paint. The conversation owner share is empty, while the sidebar owner share contains only `collapsed` and `width`; registrants obtain business data from standard hooks and actions from their own inject faces.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏宽度固定(只会收缩详情栏,然后将其自动关闭);关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。该包还提供主题呈现器:它消费解析后的 `ctx.theme` 快照,并将其投影到 document(用 `html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,并将主题的别名 token 设为 body 上的内联变量)。
|
||||
外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏的缩放边界是不可见命中条带,详情栏边界则保留其浮动胶囊;让步期间只有详情栏会收缩并随后自动关闭。关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。该包还提供主题呈现器:它消费解析后的 `ctx.theme` 快照,并将其投影到 document(用 `html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,并将主题的别名 token 设为 body 上的内联变量)。
|
||||
|
||||
AppFrame 始终挂载会话栏和详情栏;已连接 Session 通过 `SessionProvider` 渲染。布局 store 是瞬时状态,两个面板均以默认宽度启动,且从不读写 `localStorage`。hero 和其他未选中状态会将详情栏的渲染宽度派生为零,但不会改变存储的首选宽度。AppFrame 会跨越这些状态保留最后一个非 blank 会话 id:首个会话以默认宽度打开;返回同一会话时恢复其未改变的宽度;选择不同会话时,详情栏会在绘制前关闭。会话 owner share 为空,侧边栏 owner share 只包含 `collapsed` 和 `width`;注册方通过标准钩子获取业务数据,并从各自的 inject 表层获取操作。
|
||||
|
||||
|
||||
@@ -49,9 +49,8 @@
|
||||
}
|
||||
|
||||
/* Drag handles are frame children (columns clip overflow): an 8px hit strip
|
||||
centered on the column border via inline left, above column content. The
|
||||
visible pill (12x32 r10, riding the border at vertical center) is the figma
|
||||
Handle component; the hit strip stays wider than the pill. */
|
||||
centered on the column border via inline left, above column content. Details
|
||||
adds a visible 12x32 pill at vertical center; sidebar keeps only the hit strip. */
|
||||
.handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -76,7 +75,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.handle::after {
|
||||
.handle[data-side='details']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -88,23 +87,22 @@
|
||||
box-sizing: border-box;
|
||||
background: var(--dsw-alias-button-floating-fill);
|
||||
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
||||
/* Hover affordance: the pill hides until the pointer is over the owning
|
||||
column (data-side pairs handle and column), the strip itself, or a drag. */
|
||||
/* Hover affordance: the details pill hides until the pointer is over its
|
||||
column, the strip itself, or a drag. */
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.sidebarCol:hover ~ .handle[data-side='sidebar']::after,
|
||||
.detailsCol:hover ~ .handle[data-side='details']::after,
|
||||
.handle:hover::after,
|
||||
.handle[data-dragging='true']::after {
|
||||
.handle[data-side='details']:hover::after,
|
||||
.handle[data-side='details'][data-dragging='true']::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.handle:hover::after,
|
||||
.handle[data-dragging='true']::after {
|
||||
.handle[data-side='details']:hover::after,
|
||||
.handle[data-side='details'][data-dragging='true']::after {
|
||||
background: var(--dsw-alias-button-floating-hover);
|
||||
border-color: var(--dsw-alias-border-l3);
|
||||
}
|
||||
Reference in New Issue
Block a user