diff --git a/src/web/public/mobile-handlers.js b/src/web/public/mobile-handlers.js index 6ce78f9b..e76e5017 100644 --- a/src/web/public/mobile-handlers.js +++ b/src/web/public/mobile-handlers.js @@ -301,13 +301,12 @@ const KeyboardHandler = { if (this.keyboardVisible) { const keyboardHeight = this.initialViewportHeight - (window.visualViewport.height || window.innerHeight); + const accessoryBar = document.querySelector('.keyboard-accessory-bar'); - // Toolbar and accessory bar: only transform on phones/small tablets where - // they are position:fixed. On iPad (≥768px) they're in normal flow inside - // the shrunk .app container and already sit above the keyboard. if (isSmallMedium) { + // Phones/small tablets: toolbar and accessory bar are position:fixed + // via CSS. Use translateY to lift them above the keyboard. const toolbar = document.querySelector('.toolbar'); - const accessoryBar = document.querySelector('.keyboard-accessory-bar'); const main = document.querySelector('.main'); const layoutHeight = window.innerHeight; @@ -324,25 +323,25 @@ const KeyboardHandler = { const cjkInputHeight = cjkInput?.classList.contains('cjk-input-visible') ? 44 : 0; main.style.paddingBottom = `${84 + cjkInputHeight}px`; } + } else if (keyboardHeight > 0) { + // iPad: use direct bottom positioning (translateY unreliable — + // iOS auto-scrolls the visual viewport, making keyboardOffset ≈ 0). + if (accessoryBar) { + accessoryBar.style.bottom = `${keyboardHeight}px`; + } } // CJK textarea positioning (always position:fixed on touch devices). if (cjkInput?.classList.contains('cjk-input-visible') && keyboardHeight > 0) { if (isSmallMedium) { - // Phones: use translateY like toolbar/accessory bar. The CSS bottom - // (84px) is relative to the layout viewport; translateY lifts it - // above the keyboard. This works on phones where iOS doesn't - // auto-scroll the visual viewport for the CJK textarea. + // Phones: use translateY like toolbar/accessory bar. const layoutHeight = window.innerHeight; const visualBottom = window.visualViewport.offsetTop + window.visualViewport.height; const keyboardOffset = Math.max(0, layoutHeight - visualBottom); cjkInput.style.transform = keyboardOffset > 0 ? `translateY(${-keyboardOffset}px)` : ''; cjkInput.style.bottom = ''; } else { - // iPad: use direct bottom positioning from keyboard height. - // translateY breaks on iPad because iOS auto-scrolls the visual - // viewport when the CJK textarea receives focus, making - // keyboardOffset unreliable (approaches 0). + // iPad: direct bottom = keyboard + accessory bar height. cjkInput.style.bottom = `${keyboardHeight + 44}px`; cjkInput.style.transform = ''; } @@ -364,6 +363,7 @@ const KeyboardHandler = { } if (accessoryBar) { accessoryBar.style.transform = ''; + accessoryBar.style.bottom = ''; } if (cjkInput) { cjkInput.style.transform = ''; diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 3838a9b9..e429ee0f 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -2099,178 +2099,9 @@ html.mobile-init .file-browser-panel { } -/* ============================================================================ - Keyboard Accessory Bar — all mobile/tablet sizes - Visual styles extracted from phone breakpoint so they apply on iPad too. - Phone-specific positioning (position: fixed) remains in @media (max-width: 430px). - ============================================================================ */ -.keyboard-accessory-bar { - display: none; - height: 44px; - background: #1a1a1a; - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding: 6px 8px; - gap: 8px; - align-items: center; - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; - z-index: 51; -} - -.keyboard-accessory-bar.visible { - display: flex; -} - -.keyboard-accessory-bar::-webkit-scrollbar { - display: none; -} - -.accessory-btn { - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - gap: 4px; - padding: 6px 12px; - background: #2a2a2a; - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 6px; - color: #e5e5e5; - font-size: 0.65rem; - font-weight: 500; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; -} - -.accessory-btn.confirming { - background: #6b4f00; - border-color: #b8860b; - color: #ffd54f; -} - -.accessory-btn:active { - background: #3a3a3a; -} - -.accessory-btn svg { - width: 14px; - height: 14px; -} - -.accessory-btn-arrow { - padding: 6px 10px; - background: #2563eb; - border-color: rgba(59, 130, 246, 0.5); - color: #fff; -} - -.accessory-btn-arrow:active { - background: #1d4ed8; -} - -.accessory-btn-dismiss { - margin-left: auto; - flex: 1 1 0; - max-width: 80px; - padding: 10px 8px; - background: #334d6e; - border-color: rgba(100, 150, 200, 0.4); - color: #c0d4e8; - font-weight: 600; -} - -.accessory-btn-dismiss svg { - width: 20px; - height: 20px; -} - -.accessory-btn-dismiss:active { - background: #3d5f85; -} - -/* ============================================================================ - Paste Overlay — all mobile/tablet sizes - Extracted from phone breakpoint so it applies on iPad too. - ============================================================================ */ -.paste-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - z-index: 10000; - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 15vh; -} - -.paste-dialog { - background: var(--bg-secondary, #1e1e2e); - border: 1px solid var(--border-color, #444); - border-radius: 12px; - padding: 12px; - width: calc(100% - 24px); - max-width: 400px; -} - -.paste-textarea { - width: 100%; - min-height: 80px; - max-height: 200px; - background: var(--bg-primary, #0d0d14); - color: var(--text-primary, #e0e0e0); - border: 1px solid var(--border-color, #444); - border-radius: 8px; - padding: 8px; - font-family: inherit; - font-size: 16px; - resize: none; - box-sizing: border-box; -} - -.paste-textarea:focus { - outline: none; - border-color: var(--accent-color, #7aa2f7); -} - -.paste-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 10px; -} - -.paste-cancel, .paste-new, .paste-send, .paste-image { - padding: 8px 18px; - border: none; - border-radius: 8px; - font-size: 14px; - cursor: pointer; -} - -.paste-image { - margin-right: auto; - background: var(--bg-tertiary, #333); - color: var(--accent-color, #7aa2f7); - border: 1px solid var(--accent-color, #7aa2f7); -} - -.paste-cancel { - background: var(--bg-tertiary, #333); - color: var(--text-secondary, #aaa); -} - -.paste-new { - background: var(--bg-tertiary, #333); - color: var(--accent-color, #7aa2f7); - border: 1px solid var(--accent-color, #7aa2f7); -} - -.paste-send { - background: var(--accent-color, #7aa2f7); - color: #fff; - font-weight: 600; -} +/* Keyboard accessory bar + paste overlay base styles moved to styles.css + (always loaded — covers iPad landscape where mobile.css doesn't load). + Phone-specific overrides remain in @media (max-width: 430px) above. */ /* ============================================================================ iOS Safari Specific Fixes diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 7a397d95..2f86375a 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -9328,6 +9328,193 @@ body.touch-device.cjk-input-visible .main { padding-bottom: calc(84px + var(--safe-area-bottom)); } +/* ═══════════════════════════════════════════════════════════════ + Keyboard Accessory Bar + Base styles in styles.css (always loaded) so iPad landscape (≥1024px, + where mobile.css doesn't load) still gets dark styling. Phone overrides + remain in mobile.css @media (max-width: 430px). + ═══════════════════════════════════════════════════════════════ */ + +.keyboard-accessory-bar { + display: none; + height: 44px; + background: #1a1a1a; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding: 6px 8px; + gap: 8px; + align-items: center; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + z-index: 51; +} + +.keyboard-accessory-bar.visible { + display: flex; +} + +.keyboard-accessory-bar::-webkit-scrollbar { + display: none; +} + +/* Touch devices: fixed positioning to float above keyboard. + Phone CSS (mobile.css) adds bottom + safe-area padding; + iPad JS sets bottom dynamically via updateLayoutForKeyboard(). */ +.touch-device .keyboard-accessory-bar { + position: fixed; + left: 0; + right: 0; + transition: transform 0.15s ease-out; + will-change: transform; +} + +.accessory-btn { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + gap: 4px; + padding: 6px 12px; + background: #2a2a2a; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + color: #e5e5e5; + font-size: 0.65rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.accessory-btn.confirming { + background: #6b4f00; + border-color: #b8860b; + color: #ffd54f; +} + +.accessory-btn:active { + background: #3a3a3a; +} + +.accessory-btn svg { + width: 14px; + height: 14px; +} + +.accessory-btn-arrow { + padding: 6px 10px; + background: #2563eb; + border-color: rgba(59, 130, 246, 0.5); + color: #fff; +} + +.accessory-btn-arrow:active { + background: #1d4ed8; +} + +.accessory-btn-dismiss { + margin-left: auto; + flex: 1 1 0; + max-width: 100px; + padding: 10px 8px; + background: #2563eb; + border-color: rgba(59, 130, 246, 0.5); + color: #fff; + font-weight: 600; +} + +.accessory-btn-dismiss svg { + width: 22px; + height: 22px; +} + +.accessory-btn-dismiss:active { + background: #1d4ed8; +} + +/* ═══════════════════════════════════════════════════════════════ + Paste Overlay + Base styles in styles.css so iPad landscape works. + ═══════════════════════════════════════════════════════════════ */ + +.paste-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 10000; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 15vh; +} + +.paste-dialog { + background: var(--bg-secondary, #1e1e2e); + border: 1px solid var(--border-color, #444); + border-radius: 12px; + padding: 12px; + width: calc(100% - 24px); + max-width: 400px; +} + +.paste-textarea { + width: 100%; + min-height: 80px; + max-height: 200px; + background: var(--bg-primary, #0d0d14); + color: var(--text-primary, #e0e0e0); + border: 1px solid var(--border-color, #444); + border-radius: 8px; + padding: 8px; + font-family: inherit; + font-size: 16px; + resize: none; + box-sizing: border-box; +} + +.paste-textarea:focus { + outline: none; + border-color: var(--accent-color, #7aa2f7); +} + +.paste-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 10px; +} + +.paste-cancel, .paste-new, .paste-send, .paste-image { + padding: 8px 18px; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; +} + +.paste-image { + margin-right: auto; + background: var(--bg-tertiary, #333); + color: var(--accent-color, #7aa2f7); + border: 1px solid var(--accent-color, #7aa2f7); +} + +.paste-cancel { + background: var(--bg-tertiary, #333); + color: var(--text-secondary, #aaa); +} + +.paste-new { + background: var(--bg-tertiary, #333); + color: var(--accent-color, #7aa2f7); + border: 1px solid var(--accent-color, #7aa2f7); +} + +.paste-send { + background: var(--accent-color, #7aa2f7); + color: #fff; + font-weight: 600; +} + /* ═══════════════════════════════════════════════════════════════ Orchestrator Panel ═══════════════════════════════════════════════════════════════ */