mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(kimi-web): improve mobile safe-area handling
This commit is contained in:
parent
166e404a92
commit
2e1cceb2c7
11 changed files with 74 additions and 24 deletions
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="64x64" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)" />
|
||||
|
|
|
|||
|
|
@ -1155,10 +1155,10 @@ function openPr(url: string): void {
|
|||
.auth-page {
|
||||
align-items: flex-start;
|
||||
padding:
|
||||
max(48px, env(safe-area-inset-top))
|
||||
max(20px, env(safe-area-inset-right))
|
||||
max(24px, env(safe-area-inset-bottom))
|
||||
max(20px, env(safe-area-inset-left));
|
||||
max(48px, var(--safe-top))
|
||||
max(20px, var(--safe-right))
|
||||
max(24px, var(--safe-bottom))
|
||||
max(20px, var(--safe-left));
|
||||
}
|
||||
.auth-page-copy h1 {
|
||||
font-size: 26px;
|
||||
|
|
|
|||
|
|
@ -295,7 +295,9 @@ onUnmounted(() => {
|
|||
.toasts {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(76px + env(safe-area-inset-bottom));
|
||||
/* Sit just above the chat dock; --dock-h already includes the dock's own
|
||||
safe-area padding, so no extra safe-bottom term is needed. */
|
||||
bottom: calc(var(--dock-h, 76px) + 8px);
|
||||
width: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- pending question/approval cards, and the composer. Only rendered inside a -->
|
||||
<!-- chat-pane group so it never leaks into files/tasks/preview/btw panes. -->
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ActivationBadges, ApprovalBlock, ConversationStatus, PermissionMode, QueuedPromptView, TaskItem, TodoView, UIQuestion } from '../../types';
|
||||
import type { AppGoal, AppModel, AppSkill, QuestionResponse, ThinkingLevel } from '../../api/types';
|
||||
|
|
@ -87,6 +87,7 @@ const composerRef = ref<{
|
|||
} | null>(null);
|
||||
const workPanelRef = ref<HTMLElement | null>(null);
|
||||
const workbarRef = ref<HTMLElement | null>(null);
|
||||
const dockRef = ref<HTMLElement | null>(null);
|
||||
|
||||
function loadForEdit(value: string): boolean {
|
||||
// The nested Composer is only rendered in ChatDock's v-else — when a pending
|
||||
|
|
@ -124,17 +125,36 @@ watch(
|
|||
{ immediate: true },
|
||||
);
|
||||
|
||||
let dockResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
function publishDockHeight(): void {
|
||||
// Border-box height of the dock, exposed so fixed overlays (e.g. toasts) can
|
||||
// anchor just above the composer. offsetHeight includes the dock's own
|
||||
// safe-area padding, so consumers don't need to add safe-bottom again.
|
||||
const height = dockRef.value?.offsetHeight ?? 0;
|
||||
document.documentElement.style.setProperty('--dock-h', `${height}px`);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof ResizeObserver !== 'function' || !dockRef.value) return;
|
||||
dockResizeObserver = new ResizeObserver(publishDockHeight);
|
||||
dockResizeObserver.observe(dockRef.value);
|
||||
publishDockHeight();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('mousedown', onDocumentMouseDown, true);
|
||||
}
|
||||
dockResizeObserver?.disconnect();
|
||||
dockResizeObserver = null;
|
||||
});
|
||||
|
||||
defineExpose({ loadForEdit, loadAttachmentsForEdit, focus });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-dock" :class="[mobile ? 'align-mobile' : 'align-center']" @click.stop>
|
||||
<div ref="dockRef" class="chat-dock" :class="[mobile ? 'align-mobile' : 'align-center']" @click.stop>
|
||||
<Transition name="dock-panel">
|
||||
<div
|
||||
ref="workPanelRef"
|
||||
|
|
@ -357,12 +377,10 @@ defineExpose({ loadForEdit, loadAttachmentsForEdit, focus });
|
|||
|
||||
@media (max-width: 640px) {
|
||||
.chat-dock {
|
||||
--dock-inline-left: max(12px, env(safe-area-inset-left));
|
||||
--dock-inline-right: max(12px, env(safe-area-inset-right));
|
||||
}
|
||||
.chat-dock.align-mobile {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
/* Inline (landscape) safe-area lives here only; the inner composer /
|
||||
workbar read --dock-inline-* so the inset is applied exactly once. */
|
||||
--dock-inline-left: max(12px, var(--safe-left));
|
||||
--dock-inline-right: max(12px, var(--safe-right));
|
||||
}
|
||||
.dock-work-panel {
|
||||
left: 10px;
|
||||
|
|
|
|||
|
|
@ -1157,7 +1157,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
|
|||
.chat {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 14px max(12px, env(safe-area-inset-right)) 18px max(12px, env(safe-area-inset-left));
|
||||
padding: 14px max(12px, var(--safe-right)) 18px max(12px, var(--safe-left));
|
||||
}
|
||||
.u-bub {
|
||||
max-width: min(88%, calc(100vw - 52px));
|
||||
|
|
|
|||
|
|
@ -1941,9 +1941,9 @@ function selectModel(modelId: string): void {
|
|||
.composer {
|
||||
padding:
|
||||
9px
|
||||
var(--dock-inline-right, max(12px, env(safe-area-inset-right)))
|
||||
max(24px, env(safe-area-inset-bottom))
|
||||
var(--dock-inline-left, max(12px, env(safe-area-inset-left)));
|
||||
var(--dock-inline-right, max(12px, var(--safe-right)))
|
||||
max(24px, var(--safe-bottom))
|
||||
var(--dock-inline-left, max(12px, var(--safe-left)));
|
||||
}
|
||||
.composer-card {
|
||||
border-radius: var(--radius-xl);
|
||||
|
|
|
|||
|
|
@ -944,6 +944,14 @@ function onKeyDown(event: KeyboardEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
// When the on-screen keyboard opens, browsers without interactive-widget support
|
||||
// fire a visualViewport resize instead of shrinking the layout viewport. Re-follow
|
||||
// the tail so the latest turn stays visible above the keyboard. No-op while the
|
||||
// user has manually scrolled away (following === false).
|
||||
function onVisualViewportResize(): void {
|
||||
if (following.value) scheduleFollow(false);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (typeof MutationObserver === 'function') {
|
||||
|
|
@ -974,6 +982,7 @@ onMounted(() => {
|
|||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
window.visualViewport?.addEventListener('resize', onVisualViewportResize);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -992,6 +1001,7 @@ onUnmounted(() => {
|
|||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
}
|
||||
window.visualViewport?.removeEventListener('resize', onVisualViewportResize);
|
||||
});
|
||||
|
||||
function focusComposer(): void {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ onUnmounted(() => {
|
|||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
||||
padding-bottom: max(16px, var(--safe-bottom));
|
||||
}
|
||||
|
||||
/* Slide-up + fade transition for the whole sheet (scrim fades, panel slides). */
|
||||
|
|
|
|||
|
|
@ -593,11 +593,11 @@ watch(
|
|||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 14px max(14px, env(safe-area-inset-right)) 14px max(14px, env(safe-area-inset-left));
|
||||
padding: 14px max(14px, var(--safe-right)) 14px max(14px, var(--safe-left));
|
||||
}
|
||||
.group-title {
|
||||
padding-left: max(14px, env(safe-area-inset-left));
|
||||
padding-right: max(14px, env(safe-area-inset-right));
|
||||
padding-left: max(14px, var(--safe-left));
|
||||
padding-right: max(14px, var(--safe-right));
|
||||
}
|
||||
.srow-main {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
|||
|
|
@ -90,9 +90,11 @@ const statusText = computed<string>(() =>
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 50px;
|
||||
/* Grow the bar by the top inset so the 50px content row stays below the
|
||||
status bar / notch in standalone PWA mode and landscape. */
|
||||
height: calc(50px + var(--safe-top));
|
||||
flex: none;
|
||||
padding: 0 12px;
|
||||
padding: var(--safe-top) max(12px, var(--safe-right)) 0 max(12px, var(--safe-left));
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
background: var(--color-bg);
|
||||
font-family: var(--font-ui);
|
||||
|
|
|
|||
|
|
@ -213,6 +213,24 @@ summary {
|
|||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Safe-area insets + chat-dock metrics ---------------------------------------
|
||||
Centralised so mobile layout reads one consistent source instead of each
|
||||
component calling env(safe-area-inset-*) with its own fallback. Falls back to
|
||||
0px where the UA doesn't expose insets (most desktops, older Android);
|
||||
pair with max(min, var(--safe-bottom)) so gestures / home indicators keep a
|
||||
minimum gutter even when the inset reports 0. The mobile footer (composer),
|
||||
bottom sheets and the toast layer all consume these. */
|
||||
:root {
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-right: env(safe-area-inset-right, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-left: env(safe-area-inset-left, 0px);
|
||||
/* Border-box height of the chat dock, written by ChatDock via ResizeObserver.
|
||||
Used to anchor fixed overlays (toasts, floating hints) just above the
|
||||
composer — dynamic so multi-line input growth keeps them clear. */
|
||||
--dock-h: 0px;
|
||||
}
|
||||
|
||||
/* -- icon primitive (design-system §02) -------------------------------------
|
||||
Shared by the <Icon> component and iconSvg() v-html strings (lib/icons.ts).
|
||||
Icons are Remix Icon (fill="currentColor" on a 24x24 grid), so colour follows
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue