mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-16 12:14:52 +00:00
* fix(web-shell): mobile UX — safe areas, overscroll, native-app feel
The Web Shell felt like a regular web page on iPhone:
- white bands at the top (status bar) and bottom (home indicator) when
scrolling on notched devices,
- iOS Safari's rubber-band overscroll briefly exposed the default white
browser canvas,
- 300ms tap delay and the blue tap-flash gave away that buttons were
HTML elements,
- long-pressing UI chrome (hamburger, composer buttons) popped the
browser's "Copy / Look Up" callout,
- tapping the composer auto-zoomed the viewport because the editor
font-size was 14px (below iOS's 16px threshold),
- the soft keyboard pushed the composer off-screen instead of resizing
the content area.
This commit fixes all of the above so the Web Shell reads as a native
chat app on mobile, while preserving accessibility (users can still
pinch-zoom message content and long-press to copy code).
Safe-area / overscroll fixes:
- index.html: add `viewport-fit=cover` so content extends behind the
status bar and home indicator; add a `theme-color` meta; add an
inline script that runs before first paint to read the stored theme
and apply `.theme-dark` / `.theme-light` to `<html>` so the canvas
background matches the app theme from the very first frame.
- main.tsx: add a `useEffect` that keeps `<html>` class and
`<meta theme-color>` in sync when the React theme changes (covers
the `?theme=` URL parameter and in-app toggling).
- standalone.css: set html/body background per theme class and add
`overscroll-behavior-y: none` to suppress the pull-to-refresh bounce
(the chat pane handles its own scroll internally).
- App.module.css: add `padding-top: env(safe-area-inset-top)` and
`padding-bottom: env(safe-area-inset-bottom)` on `.app`. On desktop
and non-notched devices every inset evaluates to 0 so nothing changes.
Native-app feel:
- index.html: add `interactive-widget=resizes-content` to the viewport
meta so the iOS soft keyboard resizes the content area instead of
panning / zooming the viewport.
- standalone.css: set `touch-action: manipulation` on html/body to
remove the 300ms tap delay and disable double-tap zoom (we
intentionally do NOT set `user-scalable=no` / `maximum-scale=1` —
those break WCAG 1.4.4 and iOS 10+ already ignores them); add
`-webkit-tap-highlight-color: transparent` to remove the iOS blue
flash; add `text-size-adjust: 100%` to stop iOS bumping the font
size on orientation change; apply `user-select: none` +
`-webkit-touch-callout: none` to all descendants of html so long
presses on UI chrome no longer trigger the browser's callout menu;
re-enable selection on message content, the CodeMirror editor
surface, and native inputs via a `:where()` rule.
- standalone.css: add an `@supports` + `@media` rule that bumps the
composer / input font-size to 16px on iPhone only, preventing iOS
Safari's auto-zoom on focus while preserving the 14px desktop look.
- MessageItem.tsx: wrap each rendered message in a
`data-user-selectable="true"` div so users can still long-press /
drag-select reply text (the blanket `user-select: none` on
`html *` would otherwise disable selection on message bodies too).
Tested manually on iPhone over `qwen serve --hostname 0.0.0.0`.
Authored-on: Qwen Code Web Shell (mobile) ~(¯▽¯~)~
Signed-off-by: pomelo-nwu <czynwu@outlook.com>
* fix(web-shell): address review feedback — specificity, safe-area, minor fixes
- Fix :where() specificity bug: plain [data-user-selectable] at (0,1,0)
beats html * at (0,0,1), restoring text selection on message bodies
- Add left/right safe-area insets for landscape notch phones
- Replace silent outer catch with console.warn for theme-init script
- Update MessageItem comment to match code (wrapper now always applied)
- Add Android-only comment for interactive-widget=resizes-content
- Use explicit theme class removal instead of stripping all theme-* prefixes
* fix(web-shell): increase mobile font-size specificity, add drawer safe-area padding
- Scope mobile 16px font override under .app (0,3,0) to beat
.editorArea .cm-content (0,2,0) from ChatEditor.module.css
- Add safe-area top/bottom padding to mobileDrawerOpen so sidebar
content stays clear of status bar notch and home indicator
* fix(web-shell): scope font-size override to composer, add dialog selection
- Replace dead `.app` selector (CSS module hashes the class name) with
`#root [data-composer]` at specificity (1,2,0), reliably beating the
EditorView.theme `.cm-content` at (0,2,0). Add `data-composer`
attribute to the editorShell div in ChatEditor.tsx.
- Add `[role='dialog']` and `[role='dialog'] *` to the selection
re-enable block so tool-approval overlays, MCP config, theme picker,
and other portal dialogs remain selectable on mobile.
* fix(web-shell): address review round 2 — drawer, inputs, color-scheme, theme URL
- Exclude mobile drawer from `[role='dialog']` selection re-enable
(drawer gets role=dialog when open, which would undo user-select:none
on sidebar controls)
- Add horizontal safe-area padding to the fixed drawer for landscape
notch iPhones
- Strip `?theme=` from URL via replaceState to prevent bookmarked /
shared URLs from permanently overriding stored theme preference
- Broaden font-size 16px override to cover all text inputs on iPhone
(dialog inputs, sidebar search, etc.), not just the composer
- Add `color-scheme: dark/light` to theme blocks so native form
controls (scrollbars, selection handles, autofill) match the theme
* fix(web-shell): review round 3 — touch-only user-select, iPad auto-zoom, URL params
- Scope `html * { user-select: none }` to `@media (hover: none) and
(pointer: coarse)` so desktop users retain normal text selection in
sidebar, error messages, and toasts
- Replace deprecated `max-device-width` media query with
`(hover: none) and (pointer: coarse)` for auto-zoom prevention,
covering iPad in addition to iPhone
- Strip `?language=` and `?lang=` from URL alongside `?theme=` to
prevent bookmarked URLs from permanently overriding stored preferences
- Fix stale `:where()` reference in comment
---------
Signed-off-by: pomelo-nwu <czynwu@outlook.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
63 lines
4.3 KiB
HTML
63 lines
4.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<!--
|
|
`interactive-widget=resizes-content` is Chrome-on-Android-only (108+);
|
|
iOS Safari silently ignores it — the virtual keyboard always overlays
|
|
the viewport there. Keyboard-aware layout on iOS requires the
|
|
VirtualKeyboard API or `visualViewport` resize events instead.
|
|
-->
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
|
/>
|
|
<!--
|
|
Status bar color follows the active theme; the inline script below
|
|
updates this meta tag in the same tick it applies the theme class so
|
|
mobile browsers never flash the default (white) bar.
|
|
-->
|
|
<meta name="theme-color" content="#0d0d0d" />
|
|
<title>Qwen Code Web chat</title>
|
|
<!--
|
|
Apply the stored theme class + theme-color before first paint so the
|
|
<html> background matches the app theme on mobile. Without this, the
|
|
iOS Safari rubber-band overscroll and the status-bar / home-indicator
|
|
safe areas briefly show the default white canvas. Storage key mirrors
|
|
THEME_STORAGE_KEY in main.tsx.
|
|
-->
|
|
<script>
|
|
!function(){try{var k='qwen-code-web-shell-theme',p=new URLSearchParams(location.search),t=p.get('theme');if(t!=='dark'&&t!=='light'){try{t=localStorage.getItem(k)}catch(_){t=null}}if(t!=='dark'&&t!=='light')t='dark';document.documentElement.classList.add('theme-'+t);var m=document.querySelector('meta[name=theme-color]');if(m)m.setAttribute('content',t==='light'?'#ffffff':'#0d0d0d')}catch(e){console.warn('theme-init:',e)}}();
|
|
</script>
|
|
<!--
|
|
Favicon is inlined as a data: URI rather than a /favicon.svg file because
|
|
the daemon's static server only exposes /assets/* and / (see
|
|
packages/cli/src/serve/web-shell-static.ts) — a file at the dist root
|
|
would never be reachable. The data: URI is allowed by the shell CSP
|
|
(img-src 'self' data: blob:). The encodeURIComponent(svg) encoding
|
|
technique mirrors packages/web-templates/src/export-html/build.mjs;
|
|
the artwork is from packages/desktop/apps/webui/src/public/favicon.svg
|
|
(the purple #6D44E8 Qwen mark, legible on both light and dark browser
|
|
tab bars).
|
|
-->
|
|
<link
|
|
rel="icon"
|
|
type="image/svg+xml"
|
|
href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%20141.38%20140%22%3E%0A%20%20%3Cpath%0A%20%20%20%20fill%3D%22%236D44E8%22%0A%20%20%20%20d%3D%22m140.93%2085-16.35-28.33-1.93-3.34%208.66-15a3.323%203.323%200%200%200%200-3.34l-9.62-16.67c-.3-.51-.72-.93-1.22-1.22s-1.07-.45-1.67-.45H82.23l-8.66-15a3.33%203.33%200%200%200-2.89-1.67H51.43c-.59%200-1.17.16-1.66.45-.5.29-.92.71-1.22%201.22L32.19%2029.98l-1.92%203.33H12.96c-.59%200-1.17.16-1.66.45-.5.29-.93.71-1.22%201.22L.45%2051.66a3.323%203.323%200%200%200%200%203.34l18.28%2031.67-8.66%2015a3.32%203.32%200%200%200%200%203.34l9.62%2016.67c.3.51.72.93%201.22%201.22s1.07.45%201.67.45h36.56l8.66%2015a3.35%203.35%200%200%200%202.89%201.67h19.25a3.34%203.34%200%200%200%202.89-1.67l18.28-31.67h17.32c.6%200%201.17-.16%201.67-.45s.92-.71%201.22-1.22l9.62-16.67a3.323%203.323%200%200%200%200-3.34ZM51.44%203.33%2061.07%2020l-9.63%2016.66h76.98l-9.62%2016.66H45.67l-11.54-20zM57.21%20120H22.58l9.63-16.67h19.25l-38.5-66.67h19.25l9.62%2016.67L68.78%20100l-11.55%2020Zm61.59-33.34-9.62-16.67-38.49%2066.67-9.63-16.67%209.63-16.66%2026.94-46.67h23.1l17.32%2030z%22%0A%20%20%2F%3E%0A%3C%2Fsvg%3E"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<!--
|
|
Prevent React 19 dev-mode OOM crash: logComponentRender() calls
|
|
performance.measure() with component props as structured-clone detail.
|
|
Large transcript data exceeds the clone memory limit. This patch
|
|
silently drops the failing measure call; production builds are unaffected
|
|
because logComponentRender only exists in react-dom development bundles.
|
|
-->
|
|
<script>
|
|
!function(){if(typeof performance==='undefined'||!performance.measure)return;var m=performance.measure.bind(performance);performance.measure=function(){try{return m.apply(this,arguments)}catch(e){if(e instanceof DOMException&&e.name==='DataCloneError')return;throw e}}}();
|
|
</script>
|
|
<div id="root"></div>
|
|
<script type="module" src="/main.tsx"></script>
|
|
</body>
|
|
</html>
|