diff --git a/src/features/composer/components/Composer.tsx b/src/features/composer/components/Composer.tsx index 574a7baf0..0e3769471 100644 --- a/src/features/composer/components/Composer.tsx +++ b/src/features/composer/components/Composer.tsx @@ -118,6 +118,13 @@ export function Composer({ const textareaRef = externalTextareaRef ?? internalRef; const isDictationBusy = dictationState !== "idle"; const canSend = text.trim().length > 0 || attachedImages.length > 0; + const isMac = + typeof navigator !== "undefined" && + ((navigator as Navigator & { userAgentData?: { platform?: string } }) + .userAgentData?.platform ?? navigator.platform ?? "") + .toLowerCase() + .includes("mac"); + const sendShortcutLabel = isMac ? "⌘+Enter" : "Ctrl+Enter"; useEffect(() => { setText((prev) => (prev === draftText ? prev : draftText)); @@ -244,6 +251,7 @@ export function Composer({ canStop={canStop} canSend={canSend} isProcessing={isProcessing} + sendShortcutLabel={sendShortcutLabel} onStop={onStop} onSend={handleSend} dictationEnabled={dictationEnabled} @@ -262,7 +270,9 @@ export function Composer({ onTextChange={handleTextChange} onSelectionChange={handleSelectionChange} onKeyDown={(event) => { - if (event.key === "Enter" && event.shiftKey) { + const isEnter = event.key === "Enter"; + const isSendShortcut = isMac ? event.metaKey : event.ctrlKey; + if (isEnter && !isSendShortcut) { event.preventDefault(); const textarea = textareaRef.current; if (!textarea) { @@ -295,7 +305,7 @@ export function Composer({ if (event.defaultPrevented) { return; } - if (event.key === "Enter" && !event.shiftKey) { + if (isEnter && isSendShortcut) { if (isDictationBusy) { event.preventDefault(); return; diff --git a/src/features/composer/components/ComposerInput.tsx b/src/features/composer/components/ComposerInput.tsx index 13cd983e9..e9f239278 100644 --- a/src/features/composer/components/ComposerInput.tsx +++ b/src/features/composer/components/ComposerInput.tsx @@ -13,6 +13,7 @@ type ComposerInputProps = { canStop: boolean; canSend: boolean; isProcessing: boolean; + sendShortcutLabel: string; onStop: () => void; onSend: () => void; dictationState?: "idle" | "listening" | "processing"; @@ -46,6 +47,7 @@ export function ComposerInput({ canStop, canSend, isProcessing, + sendShortcutLabel, onStop, onSend, dictationState = "idle", @@ -317,33 +319,38 @@ export function ComposerInput({ > {isDictating ? : } - + + ); } diff --git a/src/styles/composer.css b/src/styles/composer.css index 95ba08f57..311f15f51 100644 --- a/src/styles/composer.css +++ b/src/styles/composer.css @@ -198,6 +198,35 @@ position: relative; } +.composer-action-wrap { + display: inline-flex; + position: relative; +} + +.composer-action-wrap::after { + content: attr(data-tooltip); + position: absolute; + left: 50%; + bottom: calc(100% + 6px); + transform: translateX(-50%) translateY(4px); + padding: 4px 8px; + border-radius: 999px; + font-size: 10px; + color: var(--text-emphasis); + background: var(--surface-command); + border: 1px solid var(--border-subtle); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease, transform 120ms ease; + z-index: 2; +} + +.composer-action-wrap:hover::after { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + .composer-action--mic.is-active { border-color: rgba(120, 235, 190, 0.6); background: rgba(120, 235, 190, 0.12);