feat(web): toast anchoring and model-selection rejection banner

The toast sits 120px from the viewport top and centers over its anchor —
the composer card, so it centers on the chat column rather than the window;
a rejected model selection (e.g. picking a text-only model while the
session holds images) announces through the same banner while the in-menu
strip with Retry stays the catalog-load surface. The attachment rail
consumes every wheel tick with a vertical component: a diagonal pan keeps
its horizontal intent and nothing scrolls the conversation behind the
composer.
This commit is contained in:
creatixchu
2026-08-11 18:00:56 +08:00
parent 87e3c95027
commit dc42e6b822
17 changed files with 158 additions and 42 deletions

View File

@@ -128,10 +128,14 @@ describe('AttachmentRail', () => {
expect(scrollBy).toHaveBeenCalledWith({ left: 32, behavior: 'auto' })
fireEvent.wheel(rail, { deltaY: -1, deltaMode: WheelEvent.DOM_DELTA_PAGE })
expect(scrollBy).toHaveBeenCalledWith({ left: -60, behavior: 'auto' })
// A trackpad pan (deltaX) and a zero-delta wheel keep native behavior.
expect(fireEvent.wheel(rail, { deltaX: 12, deltaY: 30 })).toBe(true)
// A diagonal pan is consumed too — nothing vertical may escape the rail —
// and keeps its horizontal intent.
expect(fireEvent.wheel(rail, { deltaX: 12, deltaY: 30 })).toBe(false)
expect(scrollBy).toHaveBeenCalledWith({ left: 12, behavior: 'auto' })
// A purely horizontal pan and a zero-delta wheel keep native behavior.
expect(fireEvent.wheel(rail, { deltaX: 12, deltaY: 0 })).toBe(true)
fireEvent.wheel(rail, { deltaY: 0 })
expect(scrollBy).toHaveBeenCalledTimes(5)
expect(scrollBy).toHaveBeenCalledTimes(6)
})
it('pages instantly under a reduced-motion preference, smoothly otherwise', () => {