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