Keep the composer above the on-screen keyboard on the first tap

Both composers are position:sticky;bottom:0, which pins them to the bottom
of the *layout* viewport. A phone keyboard does not shrink that viewport --
it only covers it -- so the first tap on the input left the box sitting
behind the keyboard. Tapping back and focusing again appeared to work only
because the page had been scrolled during the first attempt, so the
browser's scroll-into-view landed somewhere else.

Two engines, two halves:

- Chromium honours interactive-widget=resizes-content on the viewport meta,
  which makes the layout viewport shrink when the keyboard opens, so 100dvh
  and sticky bottoms account for it on their own.
- iOS Safari ignores that flag and only shrinks the visual viewport, so
  measure the covered height from visualViewport and publish it as
  --kb-inset; the composers offset their sticky bottom by it. On browsers
  that already resized the layout viewport the measurement is ~0, so the
  same rule is a no-op there instead of a double lift.

The pages also gain a matching bottom margin so the last beat can still be
scrolled clear of a lifted composer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011BC7Dsg4gSQm3NLcZcG5SK
This commit is contained in:
Claude
2026-08-16 08:52:33 +00:00
parent 7ab2f2a38a
commit 544c599a5a
4 changed files with 63 additions and 5 deletions
+20 -4
View File
@@ -301,7 +301,16 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
/* ---------- Play page ---------- */
.play-page { display: flex; flex-direction: column; min-height: calc(100dvh - 56px); }
.play-page {
display: flex;
flex-direction: column;
min-height: calc(100dvh - 56px);
/* Room to scroll the last beat clear of a composer lifted by the keyboard
(see --kb-inset in keyboard.js); 0 on desktop and on browsers that shrink
the layout viewport themselves. Margin, not padding — .page already sets
padding and a longhand here would drop its bottom gutter. */
margin-bottom: var(--kb-inset, 0px);
}
/* Keep the Plot/Memory/Scripts/Insights toggles reachable while the story
scrolls — pin the header just below the 56px top nav. */
@@ -446,7 +455,9 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
.play-controls {
position: sticky;
bottom: 0;
/* Sits on the keyboard's top edge when one is open, on the viewport bottom
otherwise — --kb-inset is 0 unless a virtual keyboard is covering us. */
bottom: var(--kb-inset, 0px);
background: linear-gradient(to top, var(--bg) 75%, transparent);
padding: 18px 0 16px;
border-top: 1px solid var(--border);
@@ -1952,7 +1963,12 @@ button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: aut
A plain chat scratchpad, styled as a transcript rather than as the story
page: UI font, role labels, and a sticky composer at the bottom. */
.chat-page { max-width: 820px; display: flex; flex-direction: column; }
.chat-page {
max-width: 820px;
display: flex;
flex-direction: column;
margin-bottom: var(--kb-inset, 0px); /* see .play-page */
}
.chat-meta {
font-size: 0.78rem;
margin: -10px 0 16px;
@@ -2024,7 +2040,7 @@ button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: aut
.chat-composer {
position: sticky;
bottom: 0;
bottom: var(--kb-inset, 0px); /* clears the on-screen keyboard — see .play-controls */
display: flex;
gap: 10px;
align-items: flex-end;