qwen-code/packages/web-shell/client/components/ToastHost.module.css
Shaojin Wen e39781d858
feat(web-shell): add fullscreen view for the right artifact panel (#8614)
* feat(web-shell): add fullscreen view for the right artifact panel

The right panel (artifacts / subagents / review) is narrow, so long
previews and transcripts are hard to read. Add a fullscreen toggle to
the panel header that expands the panel to cover the viewport; pressing
Escape or the toggle again shrinks it back to its dock or drawer.

* test(web-shell): capture fullscreen artifact panel in visual previews

The fullscreen toggle and surface are only reachable with the artifact
panel open, so no visual scenario rendered them and the before/after
preview could not see this feature. Extend the code review artifact
scenario to expand the panel fullscreen, capture it, and assert the
Escape restore path.

* fix(web-shell): keep artifact panel mounted across fullscreen toggle (#8614)

* fix(web-shell): harden artifact panel fullscreen per review feedback (#8614)

* fix(web-shell): guard artifact drawer Escape for IME composition (#8614)

* fix(web-shell): scope toast z-index, reset dock animation flag, guard IME keyCode (#8614)

Round-4 review fixes for the artifact panel fullscreen:

- Elevate ToastHost above dialog-backdrop-tier surfaces only while the
  fullscreen surface is up (new `elevated` prop); otherwise it stays at its
  original z-index 30 so DialogShell modals keep painting above toasts. The
  old comment rested on DialogShell.module.css's dead `.backdrop` class —
  the live modal path resolves the same host variable with fallback 50.
- Reset suppressArtifactDockOpenAnimation when the dock unmounts (panel
  close or floating drawer taking over) and only set it while docked, so a
  floating<->docked flip after a fullscreen round-trip no longer mounts the
  dock without its slide-in animation.
- Extend the window Escape handler's IME guard to keyCode 229, matching the
  paired guard used everywhere else in the package: a WebKit-shaped IME
  Escape (isComposing false, keyCode 229) previously shrank a fullscreen
  panel and swallowed the native IME cancel.

Adds the suggested coverage: keyCode-229 drawer variant, docked-fullscreen
IME Escape, ask-user shrink keyboardActive history, streaming+fullscreen
Escape ordering, and the floating-interlude dock animation regression.

* fix(web-shell): portal docked fullscreen surface, gate chat shortcuts, clamp z-index (#8614)

Round-5 review fixes for the artifact panel fullscreen:

- Clamp the fullscreen surface z-index floor to max(1, backdrop - 10): a
  host setting --web-shell-dialog-backdrop-z-index below 10 gave the
  surface a negative z-index, painting the opaque panel behind the app
  background and blanking the shell.
- Add artifactPanelFullscreen to interactionBlocked: chat-only global
  shortcuts (Ctrl+L/O/Y, Shift+Tab, the btw hotkey) kept mutating the
  hidden chat behind the surface, and the btw capture-phase Escape handler
  dismissed hidden content and swallowed the Escape that shrinks the panel.
- Pad the floating drawer fullscreen content with env(safe-area-inset-*):
  the portaled drawer sits outside the padded app root, so on
  viewport-fit=cover devices the toolbar and Exit control could sit under
  the notch/status bar and bottom content behind the home indicator.
- Move the docked fullscreen surface into the top-level portal root and
  give it document-level modal semantics: a transformed, paint-contained,
  or lower-stacking host ancestor could bound the fixed panel or paint
  over it, and Tab could escape into covered host controls. The panel
  wrapper portals into a display:contents slot that the fullscreen effect
  parks in the portal root, so the SAME node survives the move (panel
  state preserved, React event delegation intact); FocusScope provides the
  Tab containment and the effect hides every outside tree from AT and
  captures stray focus — matching what the floating variant gets from
  vaul's Radix dialog. Declares @radix-ui/react-focus-scope (already in
  the tree via radix-ui) as a direct dependency.
- Portal the elevated ToastHost into the portal root: in shadow-DOM portal
  mode the fullscreen drawer surface is sealed inside the portal host's
  stacking context, so an in-tree toast painted beneath it for its whole
  auto-dismiss lifetime.
- Keep the dock animation suppression flag across dock<->floating
  hand-overs performed mid-fullscreen in both directions, so shrinking
  back to the dock never replays the slide-in on the already-open panel.

Tests: pin both hand-over suppression directions (mutation-verified), the
portal placement, the interaction gate, and toast elevation in the main
fullscreen test; add a colocated ToastHost.test.tsx for the elevated
class; fix the dead 0-measurement step in the docked-width round-trip
test (the 0 is now seeded before entering fullscreen, where the clamp
effect can actually observe it). Adds a drawer-fullscreen visual scenario
at a narrow viewport in both themes, with Escape restoring the drawer.

* fix(web-shell): repair docked fullscreen panel modal semantics (#8614)

* fix(web-shell): reset fullscreen state in the panel close commit (#8614)

Closing the artifact panel while fullscreen reset the fullscreen flag
only in the passive management effect, so one render committed with the
panel unmounted while the covered shells stayed display:none — one
painted frame of an empty shell before the chat reappeared. Batch the
fullscreen and dock-animation-flag resets into closeArtifactPanel and
the last-tab close so the recovery happens in the same commit.

Adds a regression test asserting the shells are revealed in the close's
committed frame (fails without the batched reset), and pairing coverage
for the drawer Escape pass-through: a plain Escape still closes the
floating drawer when the panel was never fullscreen.

* fix(web-shell): pull escaped focus back into the docked fullscreen surface (#8614)

* build(external-context): restore the node-only types override from main

Cherry-picks the tsconfig guard from 3037744602 (#8693), which postdates
this branch's merge base. With @types/jsdom installed, the inherited
vitest/globals types drag the DOM lib into this program and break the
for-await over response.body in http-client.ts, failing npm run build
and npm run typecheck.

* fix(web-shell): keep docked fullscreen usable in shadow DOM (#8614)

Resolve two Critical review findings on the docked fullscreen surface:

- focusin is composed, so in shadow-DOM portal mode the document-level
  pull handler and the Tab-wrap handler saw the shadow host retargeted
  as the target and snapped every focus change back onto the surface.
  Resolve the real node via composedPath() and read the active element
  from the surface's own root.
- The aria-hidden sweep recorded values still owned by a live
  hideOthers lock when the floating drawer unmounted in the same commit
  the docked surface mounted; the deferred restore then re-applied the
  lock's value and permanently hid the shell from assistive tech.
  Record null for lock-owned siblings and let the lock's unlock restore
  the original value.

Adds regression tests for both paths; both fail without their fix.

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 12:41:48 +00:00

91 lines
2.3 KiB
CSS

.host {
position: absolute;
top: 12px;
right: 12px;
z-index: 30;
display: flex;
width: min(420px, calc(100% - 24px));
flex-direction: column;
gap: 8px;
pointer-events: none;
}
/* Applied by App while the fullscreen artifact surface is up: the docked
surface covers the viewport at dialog-backdrop z - 10 and the drawer-mode
one at dialog-backdrop z, so an un-elevated toast from a panel action
would paint under the opaque surface. DialogShell modals resolve the same
dialog-backdrop variable, and no ladder slot exists above the fullscreen
surface but below modals — so while the surface is up, toasts paint above
modals too until auto-dismiss. */
.hostElevated {
/* The elevated host is portaled out of the .app positioning context, so
.host's absolute layout would resolve off-screen: against the 0x0
shadow-DOM portal host in shadow mode, and against the initial
containing block (wrong once the host page scrolls or the shell is
embedded below the document top) in light-DOM mode. Neither portal path
has a transform/filter/contain ancestor, so fixed anchors to the
viewport in both. */
position: fixed;
z-index: calc(var(--web-shell-dialog-backdrop-z-index, 50) + 10);
}
.toast {
display: grid;
grid-template-columns: minmax(0, 1fr) 28px;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 9px 8px 9px 12px;
border: 1px solid color-mix(in srgb, var(--foreground) 14%, var(--border));
border-left-width: 4px;
border-radius: var(--radius);
background-color: var(--muted);
color: var(--foreground);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.34),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
pointer-events: auto;
}
.message {
min-width: 0;
overflow-wrap: anywhere;
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.45;
}
.close {
width: 28px;
height: 28px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
line-height: 1;
}
.close:hover,
.close:focus-visible {
background: var(--subtle-bg-strong);
color: var(--foreground);
outline: none;
}
.info {
border-left-color: var(--agent-blue-500);
}
.success {
border-left-color: var(--success-color);
}
.warning {
border-left-color: var(--warning-color);
}
.error {
border-left-color: var(--error-color);
}