diff --git a/artifacts/tx-os/src/components/editable-cell.tsx b/artifacts/tx-os/src/components/editable-cell.tsx index 5a40098f..4b4e9023 100644 --- a/artifacts/tx-os/src/components/editable-cell.tsx +++ b/artifacts/tx-os/src/components/editable-cell.tsx @@ -598,10 +598,35 @@ function FormattingToolbar({ const vvTop = vv.offsetTop; const vvBottom = vv.offsetTop + (vv.height ?? window.innerHeight); - let top = rect.bottom + 4; - if (top + tbHeight > vvBottom - 4) { - const above = rect.top - tbHeight - 4; - top = Math.max(vvTop + 4, above); + // iPad / iOS adds a QuickType prediction strip ABOVE the keyboard + // (suggested words). `visualViewport.height` doesn't subtract it, + // so without padding the toolbar lands hidden under that strip. + // 48px covers both the prediction strip and the Arabic-keyboard + // shortcut row that some users have enabled. + const KEYBOARD_PREDICTION_INSET = vv.keyboardInset > 0 ? 48 : 4; + let top: number; + if (vv.keyboardInset > 0) { + // Keyboard is up — pin the toolbar to the BOTTOM of the visible + // viewport (right above the prediction strip), iMessage/Notes + // style. This keeps the color swatches + B/I/U + Save/Cancel + // reachable no matter where the active cell sits, and matches + // the user's mental model that "formatting follows the keyboard". + top = vvBottom - tbHeight - KEYBOARD_PREDICTION_INSET; + // Don't visually cover the cell being edited — if the keyboard- + // pinned slot would overlap the anchor, fall back to above the + // cell. + if (top < rect.bottom + 4 && rect.top - tbHeight - 4 >= vvTop + 4) { + top = rect.top - tbHeight - 4; + } + top = Math.max(vvTop + 4, top); + } else { + // No keyboard — prefer BELOW the cell (matches the schedule + // spec). Flip above only if below would clip the viewport. + top = rect.bottom + 4; + if (top + tbHeight > vvBottom - 4) { + const above = rect.top - tbHeight - 4; + top = Math.max(vvTop + 4, above); + } } // Clamp horizontally to the table's scroll container if we can // find one; fall back to the viewport otherwise. This keeps the