Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/kimi-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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)" />
Expand Down
8 changes: 4 additions & 4 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion apps/kimi-web/src/components/WarningToasts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a real fallback when the dock height is unset

Because style.css initializes --dock-h to 0px, the var(--dock-h, 76px) fallback here never selects 76px. On mobile states where ChatDock has not mounted yet or is not rendered (for example auth/empty-session warning toasts), the stack is placed only 8px above the viewport bottom, losing the previous safe-area/home-indicator clearance and potentially covering the toast actions. Consider leaving --dock-h unset until measured or using an explicit max()/safe-bottom fallback.

Useful? React with 👍 / 👎.

width: auto;
max-height: 50vh;
}
Expand Down
34 changes: 26 additions & 8 deletions apps/kimi-web/src/components/chat/ChatDock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/components/chat/ChatPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
6 changes: 3 additions & 3 deletions apps/kimi-web/src/components/chat/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions apps/kimi-web/src/components/chat/ConversationPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -974,6 +982,7 @@ onMounted(() => {
document.addEventListener('visibilitychange', onVisibilityChange);
document.addEventListener('keydown', onKeyDown);
}
window.visualViewport?.addEventListener('resize', onVisualViewportResize);
});
});

Expand All @@ -992,6 +1001,7 @@ onUnmounted(() => {
document.removeEventListener('visibilitychange', onVisibilityChange);
document.removeEventListener('keydown', onKeyDown);
}
window.visualViewport?.removeEventListener('resize', onVisualViewportResize);
});

function focusComposer(): void {
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/components/dialogs/BottomSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
6 changes: 3 additions & 3 deletions apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions apps/kimi-web/src/components/mobile/MobileTopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions apps/kimi-web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading