feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety
- ChatInput: e.nativeEvent.isComposing early-return added (CJK IME safety — first Enter of a composition no longer submits). Bare-Enter send path gated by !isMobile so mobile inserts a newline; send is button-only. Cmd/Ctrl+Enter and Shift+Cmd/Ctrl+Enter retained as desktop secondary bindings. Placeholder is now viewport-aware. Outer wrapper gets paddingBottom: env(safe-area-inset-bottom) so iOS home indicator doesn't overlap. - MessageBubble: ActionRow buttons (Copy / Regenerate / Fork / Trash) bumped to max-md min-h/min-w 44px; opacity-100 on mobile so actions don't hide behind a hover-to-reveal pattern. User bubble and assistant content wrapper gain break-words + min-w-0 so long unbreakable strings (URLs / paths) wrap rather than blowing out the column on narrow viewports. - ChatPane: queued-message dropdown + close X + Stop-generating button hit max-md 44px sizing. - ChatTabBar: per-tab X, +/History/Close-pane action buttons hit max-md 44px. Tab close X is force-visible on mobile (no hover-to-reveal). - M8: CodeBlock / Markdown tables / ToolCallCard already wrap overflow-x-auto pre-existing — no source change needed there; the break-words + min-w-0 additions above are the new defensive layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -266,11 +266,11 @@ function ActionRow({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity max-md:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copy()}
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:min-h-[44px] max-md:min-w-[44px]"
|
||||
aria-label="Copy message"
|
||||
title="Copy"
|
||||
>
|
||||
@@ -281,7 +281,7 @@ function ActionRow({
|
||||
type="button"
|
||||
onClick={() => void regenerate()}
|
||||
disabled={!canRegen || regenerating}
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed max-md:min-h-[44px] max-md:min-w-[44px]"
|
||||
aria-label="Regenerate message"
|
||||
title="Regenerate"
|
||||
>
|
||||
@@ -292,7 +292,7 @@ function ActionRow({
|
||||
type="button"
|
||||
onClick={() => void fork()}
|
||||
disabled={!canFork || forking}
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed max-md:min-h-[44px] max-md:min-w-[44px]"
|
||||
aria-label="Fork from here"
|
||||
title="Fork from here"
|
||||
>
|
||||
@@ -302,7 +302,7 @@ function ActionRow({
|
||||
type="button"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
disabled={!canDelete}
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-destructive disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="inline-flex items-center justify-center size-6 rounded text-muted-foreground hover:bg-muted hover:text-destructive disabled:opacity-40 disabled:cursor-not-allowed max-md:min-h-[44px] max-md:min-w-[44px]"
|
||||
aria-label="Delete message"
|
||||
title="Delete message"
|
||||
>
|
||||
@@ -476,7 +476,7 @@ export function MessageBubble({ message, sessionChats }: Props) {
|
||||
if (message.role === 'user') {
|
||||
return (
|
||||
<div className="group flex flex-col items-end gap-1">
|
||||
<div className="max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap">
|
||||
<div className="max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap break-words min-w-0">
|
||||
{message.content}
|
||||
</div>
|
||||
<ActionRow message={message} />
|
||||
@@ -495,7 +495,7 @@ export function MessageBubble({ message, sessionChats }: Props) {
|
||||
<ToolCallCard key={tc.id} toolCall={tc} />
|
||||
))}
|
||||
{(hasContent || (!hasToolCalls && isStreaming)) && (
|
||||
<div className="max-w-[90%] text-sm leading-relaxed space-y-2">
|
||||
<div className="max-w-[90%] text-sm leading-relaxed space-y-2 break-words min-w-0">
|
||||
{hasContent ? <MarkdownBody content={message.content} /> : null}
|
||||
{isStreaming && (
|
||||
<span className="inline-block w-1.5 h-3.5 align-baseline bg-muted-foreground/60 animate-pulse" />
|
||||
|
||||
Reference in New Issue
Block a user