refactor(frontend): unify chat content rail alignment
Normalize the chat content rail around the input composer as the width baseline. This introduces shared rail variables, compensates message content for the actual scrollbar gutter, and separates mobile/webview behavior so desktop, mobile web, and app shell layouts stay centered consistently. Align assistant text, markdown blocks, tables, minimal-mode summaries, expanded minimal steps, slash/runtime/git floating UI, and brief-message separators to the same horizontal rail. Remove the collapsed right resize handle and mobile menu glow while preserving the intended percentage widths for floating composer affordances. Validation: npm run build --silent 2>&1 | tail -n 5
This commit is contained in:
parent
05e8ea5e40
commit
d095531bb8
@ -123,6 +123,7 @@
|
||||
:class="{
|
||||
'chat-container--immersive': workspaceCollapsed,
|
||||
'chat-container--mobile': isMobileViewport,
|
||||
'chat-container--app-shell': isAppShell,
|
||||
'chat-container--monitor': chatDisplayMode === 'monitor',
|
||||
'has-title-ribbon': titleRibbonVisible
|
||||
}"
|
||||
@ -370,11 +371,6 @@
|
||||
@reset-context="resetGitChangesContext"
|
||||
/>
|
||||
</transition>
|
||||
<div
|
||||
v-if="!isMobileViewport && rightCollapsed && !gitChangesPanelOpen"
|
||||
class="resize-handle"
|
||||
@mousedown="startResize('right', $event)"
|
||||
></div>
|
||||
</div>
|
||||
<transition name="desktop-approval-fade">
|
||||
<div v-if="!isMobileViewport && !rightCollapsed" class="desktop-approval-overlay">
|
||||
@ -818,7 +814,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import appOptions from './app';
|
||||
import VideoPicker from './components/overlay/VideoPicker.vue';
|
||||
import { useTutorialStore } from './stores/tutorial';
|
||||
@ -832,10 +828,23 @@ const PathAuthorizationDialog = defineAsyncComponent(
|
||||
const tutorialStore = useTutorialStore();
|
||||
|
||||
const mobilePanelIcon = new URL('../icons/align-left.svg', import.meta.url).href;
|
||||
const isAppShell =
|
||||
typeof window !== 'undefined' &&
|
||||
(new URLSearchParams(window.location.search).has('app_shell') ||
|
||||
Boolean((window as any)?.AndroidThemeBridge));
|
||||
const detectAppShell = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ua = window.navigator?.userAgent || '';
|
||||
return (
|
||||
params.has('app_shell') ||
|
||||
Boolean((window as any)?.AndroidThemeBridge) ||
|
||||
/;\s*wv\)/i.test(ua)
|
||||
);
|
||||
};
|
||||
const isAppShell = ref(detectAppShell());
|
||||
onMounted(() => {
|
||||
isAppShell.value = detectAppShell();
|
||||
window.setTimeout(() => {
|
||||
isAppShell.value = detectAppShell();
|
||||
}, 300);
|
||||
});
|
||||
const mobileMenuIcons = {
|
||||
workspace: new URL('../icons/folder.svg', import.meta.url).href,
|
||||
personal: new URL('../icons/user.svg', import.meta.url).href,
|
||||
|
||||
@ -794,6 +794,7 @@ const {
|
||||
});
|
||||
const rootEl = scrollRef;
|
||||
const thinkingRefs = new Map<string, HTMLElement | null>();
|
||||
let scrollbarResizeObserver: ResizeObserver | null = null;
|
||||
let bounceTraceCount = 0;
|
||||
const BOUNCE_TRACE_MAX = 240;
|
||||
const bounceTraceLastTsByKey = new Map<string, number>();
|
||||
@ -1009,6 +1010,13 @@ function attachBounceListener() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateChatScrollbarWidth() {
|
||||
const el = scrollRef.value;
|
||||
if (!el) return;
|
||||
const scrollbarWidth = Math.max(0, el.offsetWidth - el.clientWidth);
|
||||
el.style.setProperty('--chat-scrollbar-width', `${scrollbarWidth}px`);
|
||||
}
|
||||
|
||||
watch(
|
||||
[isAtBottom, isNearBottom, escapedFromLock],
|
||||
([atBottom, nearBottom, escaped]) => {
|
||||
@ -1463,6 +1471,11 @@ function compactBriefLabel(msg: any): string {
|
||||
|
||||
onMounted(() => {
|
||||
attachBounceListener();
|
||||
updateChatScrollbarWidth();
|
||||
if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
|
||||
scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
|
||||
scrollbarResizeObserver.observe(scrollRef.value);
|
||||
}
|
||||
console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
|
||||
hasScrollRef: !!scrollRef.value
|
||||
});
|
||||
@ -1473,10 +1486,23 @@ onMounted(() => {
|
||||
|
||||
watch(scrollRef, () => {
|
||||
attachBounceListener();
|
||||
updateChatScrollbarWidth();
|
||||
if (scrollbarResizeObserver) {
|
||||
scrollbarResizeObserver.disconnect();
|
||||
scrollbarResizeObserver = null;
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
|
||||
scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
|
||||
scrollbarResizeObserver.observe(scrollRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
detachBounceListener();
|
||||
if (scrollbarResizeObserver) {
|
||||
scrollbarResizeObserver.disconnect();
|
||||
scrollbarResizeObserver = null;
|
||||
}
|
||||
if (timerHandle !== null) {
|
||||
clearInterval(timerHandle);
|
||||
timerHandle = null;
|
||||
|
||||
@ -541,12 +541,15 @@ watch(
|
||||
|
||||
<style scoped>
|
||||
.minimal-blocks-container {
|
||||
--chat-content-x: 0px;
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 摘要组 */
|
||||
.summary-group {
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-agent-system-group {
|
||||
@ -567,7 +570,8 @@ watch(
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
column-gap: 8px;
|
||||
padding: 0 6px 0 6px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
@ -633,14 +637,15 @@ watch(
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-top: 8px;
|
||||
padding: 0 6px 0 6px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 桌面端与正式文本输出左边距保持一致(text-content: 0 20px 0 15px) */
|
||||
/* 桌面端与正式文本输出左右内边距保持一致 */
|
||||
@media (min-width: 769px) {
|
||||
.summary-line-text,
|
||||
.steps-container {
|
||||
padding: 0 20px 0 15px;
|
||||
padding: 0 var(--chat-content-x);
|
||||
}
|
||||
}
|
||||
|
||||
@ -824,7 +829,7 @@ watch(
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.text-output {
|
||||
margin: 16px -9px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1868,7 +1868,7 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.floating-project-status {
|
||||
position: absolute;
|
||||
left: calc(5% - 7px);
|
||||
left: 5%;
|
||||
bottom: calc(100% + 6px);
|
||||
z-index: auto;
|
||||
width: 90%;
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
/* 聊天容器整体布局,保证聊天区可见并支持上下滚动 */
|
||||
.chat-container {
|
||||
--chat-surface-color: var(--surface-panel);
|
||||
--chat-rail-max: 900px;
|
||||
--chat-rail-fluid: 94%;
|
||||
--chat-scrollbar-width: 8px;
|
||||
--chat-rail-width: min(var(--chat-rail-max), var(--chat-rail-fluid));
|
||||
--chat-message-rail-width: min(
|
||||
var(--chat-rail-max),
|
||||
calc(var(--chat-rail-fluid) + (var(--chat-scrollbar-width) * 0.94))
|
||||
);
|
||||
--chat-content-x: 0px;
|
||||
--composer-base-height: calc(90px + var(--app-bottom-inset));
|
||||
--composer-reserved-height: var(--composer-base-height);
|
||||
--composer-growth-height: 0px;
|
||||
@ -98,7 +107,9 @@
|
||||
}
|
||||
|
||||
.messages-flow {
|
||||
width: min(960px, 100%);
|
||||
position: relative;
|
||||
left: calc(var(--chat-scrollbar-width) / 2);
|
||||
width: var(--chat-message-rail-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -161,20 +172,23 @@
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.chat-container--immersive .messages-flow {
|
||||
width: min(900px, 100%);
|
||||
}
|
||||
|
||||
.chat-container--immersive .scroll-lock-toggle {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.chat-container--mobile {
|
||||
--chat-rail-fluid: 100%;
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.chat-container--mobile.chat-container--app-shell {
|
||||
--chat-scrollbar-width: 0px !important;
|
||||
--chat-message-rail-width: var(--chat-rail-width) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
--chat-rail-fluid: 100%;
|
||||
--composer-base-height: calc(90px + var(--app-bottom-inset));
|
||||
--composer-reserved-height: var(--composer-base-height);
|
||||
}
|
||||
@ -205,6 +219,7 @@
|
||||
/* 额外的手机端样式,不依赖 mobile 类 */
|
||||
@media (max-width: 768px) {
|
||||
.messages-flow {
|
||||
left: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
@ -528,7 +543,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
width: min(760px, 92%);
|
||||
width: 100%;
|
||||
margin: 2px auto;
|
||||
color: var(--claude-text-secondary);
|
||||
user-select: none;
|
||||
@ -1178,12 +1193,17 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
||||
}
|
||||
|
||||
.text-output .text-content {
|
||||
padding: 0 20px 0 15px;
|
||||
padding: 0 var(--chat-content-x);
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.text-output .text-content .code-block-wrapper {
|
||||
margin-right: calc(var(--chat-content-x) * -1);
|
||||
margin-left: calc(var(--chat-content-x) * -1);
|
||||
}
|
||||
|
||||
.text-output pre {
|
||||
font-family:
|
||||
'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', 'Menlo', 'Monaco', 'Courier New',
|
||||
@ -1223,9 +1243,9 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
||||
.text-output .text-content .md-table-scroll,
|
||||
.text-output .text-content [data-md-table-scroll='1'] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 16px 0;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
margin: 16px calc(var(--chat-content-x) * -1);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@ -1284,7 +1304,7 @@ show-html:not([data-rendered='1'])[ratio='3:4'] {
|
||||
}
|
||||
|
||||
.text-output .text-content hr {
|
||||
margin: 32px 0;
|
||||
margin: 32px calc(var(--chat-content-x) * -1);
|
||||
border: none;
|
||||
border-top: 1px solid var(--claude-border);
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
.stadium-input-wrapper {
|
||||
position: relative;
|
||||
width: min(900px, 94%);
|
||||
width: var(--chat-rail-width, min(900px, 94%));
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
--runtime-queue-divider: rgba(15, 23, 42, 0.12);
|
||||
--runtime-queue-border: rgba(15, 23, 42, 0.12);
|
||||
position: absolute;
|
||||
left: calc(50% - 7px);
|
||||
left: 50%;
|
||||
bottom: calc(100% - 2px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
@ -137,7 +137,7 @@
|
||||
--skill-slash-motion-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
--skill-slash-visible-rows: 5;
|
||||
position: absolute;
|
||||
left: calc(50% - 7px);
|
||||
left: 50%;
|
||||
bottom: calc(100% - 2px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
@ -610,7 +610,7 @@ body[data-theme='light'] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
padding: 2px 0 0 8px;
|
||||
padding: 2px 0 0 6px;
|
||||
}
|
||||
|
||||
.stadium-input-rich .stadium-input {
|
||||
@ -942,6 +942,10 @@ body[data-theme='light'] {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.input-area {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.blank-hero-text {
|
||||
font-size: 28px;
|
||||
}
|
||||
@ -1024,10 +1028,6 @@ body[data-theme='light'] {
|
||||
@media (max-height: 900px) {
|
||||
.input-area {
|
||||
bottom: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.stadium-shell {
|
||||
width: min(900px, 98%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1578,7 +1578,7 @@
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-soft);
|
||||
box-shadow: 0 18px 48px var(--border-default);
|
||||
box-shadow: none;
|
||||
min-width: 200px;
|
||||
z-index: 1600;
|
||||
}
|
||||
@ -3224,7 +3224,7 @@ body[data-theme='dark'] {
|
||||
/* 新的下拉菜单样式 - 暗色主题 */
|
||||
.mobile-panel-menu--dropdown {
|
||||
background: var(--badge-bg) !important;
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, var(--text-primary) 50%, transparent) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.mobile-menu-item {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user