ux(fab): float above the on-screen keyboard
Two-pronged fix so the Translate + Scan FABs stay reachable when
the mobile soft keyboard is open:
- Add `interactive-widget=resizes-content` to the viewport meta —
Chrome on Android then shrinks the layout viewport when the
keyboard opens, so `position: fixed; bottom: ...` is naturally
above it. iOS Safari ignores this directive, hence:
- VisualViewport listener computes
inset = innerHeight - vv.height - vv.offsetTop
and writes it to a `--kb-inset` CSS custom property. The FAB
bottom calc uses `max(var(--kb-inset), safe-area-inset-bottom)`
so the keyboard inset wins when it's open and the home-indicator
inset wins when it isn't.
Net: tap the editor on iOS → keyboard slides up → Translate /
Scan FABs rise with it, no manual minimise required.
This commit is contained in:
@@ -17,6 +17,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
### Changed
|
### Changed
|
||||||
- "Scan image" button replaced with a floating action button at the bottom-right of the viewport, using a Material Symbols Outlined camera glyph (`photo_camera`) for consistent rendering across platforms (was a small text button in the input pane header).
|
- "Scan image" button replaced with a floating action button at the bottom-right of the viewport, using a Material Symbols Outlined camera glyph (`photo_camera`) for consistent rendering across platforms (was a small text button in the input pane header).
|
||||||
- "Translate" inline button replaced with a primary FAB using the `translate` Material Symbols icon, stacked directly under the camera FAB at the bottom-right corner (closest to the thumb on phones). The camera FAB is now the *secondary* in the stack, just above. Both 56 × 56 px, same accent fill. The translate FAB gains a pulsing-ring `busy` animation during a running translation; `goBtn.textContent` swap is gone (was meaningless for an icon-only button). `body` got `~9 rem` of bottom padding so content scrolls clear of the stack.
|
- "Translate" inline button replaced with a primary FAB using the `translate` Material Symbols icon, stacked directly under the camera FAB at the bottom-right corner (closest to the thumb on phones). The camera FAB is now the *secondary* in the stack, just above. Both 56 × 56 px, same accent fill. The translate FAB gains a pulsing-ring `busy` animation during a running translation; `goBtn.textContent` swap is gone (was meaningless for an icon-only button). `body` got `~9 rem` of bottom padding so content scrolls clear of the stack.
|
||||||
|
- FAB stack rides above the on-screen keyboard on mobile. Added `interactive-widget=resizes-content` to the viewport meta (handles Chrome on Android) and a `VisualViewport` listener that exposes the keyboard's intrusion as a `--kb-inset` CSS custom property (handles iOS Safari). The FAB `bottom` uses `max(--kb-inset, env(safe-area-inset-bottom))` so the keyboard inset wins when it's open and the home-indicator inset wins when it isn't.
|
||||||
- Crop modal:
|
- Crop modal:
|
||||||
- No default crop box. User drags on the image to draw the region; falls back to full image if they confirm without drawing.
|
- No default crop box. User drags on the image to draw the region; falls back to full image if they confirm without drawing.
|
||||||
- Buttons are now Material Symbols icons: `rotate_left`, `rotate_right`, `invert_colors` (new — see below), `check`. Cancel kept as text. The `check` (Use this) button is larger and more prominent. The Reset button was removed (rarely useful with no default box).
|
- Buttons are now Material Symbols icons: `rotate_left`, `rotate_right`, `invert_colors` (new — see below), `check`. Cancel kept as text. The `check` (Use this) button is larger and more prominent. The Reset button was removed (rarely useful with no default box).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content" />
|
||||||
<title>finrod — translator</title>
|
<title>finrod — translator</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cg fill='%23d4a566'%3E%3Cpath d='M16 1 L18 14 L31 16 L18 18 L16 31 L14 18 L1 16 L14 14 Z'/%3E%3Cpath d='M16 4 L22 16 L16 28 L10 16 Z' opacity='.55'/%3E%3C/g%3E%3C/svg%3E" />
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cg fill='%23d4a566'%3E%3Cpath d='M16 1 L18 14 L31 16 L18 18 L16 31 L14 18 L1 16 L14 14 Z'/%3E%3Cpath d='M16 4 L22 16 L16 28 L10 16 Z' opacity='.55'/%3E%3C/g%3E%3C/svg%3E" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=block" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=block" />
|
||||||
@@ -190,12 +190,15 @@
|
|||||||
.fab .material-symbols-outlined { font-size: 28px; }
|
.fab .material-symbols-outlined { font-size: 28px; }
|
||||||
.fab:hover:not(:disabled) { transform: scale(1.06); }
|
.fab:hover:not(:disabled) { transform: scale(1.06); }
|
||||||
.fab:disabled { opacity: 0.55; cursor: progress; }
|
.fab:disabled { opacity: 0.55; cursor: progress; }
|
||||||
|
/* --kb-inset is set from JS based on VisualViewport so the FABs ride
|
||||||
|
above the on-screen keyboard. When there's no keyboard it's 0 and the
|
||||||
|
safe-area inset wins via max(). */
|
||||||
.fab-primary {
|
.fab-primary {
|
||||||
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
|
bottom: calc(1.5rem + max(var(--kb-inset, 0px), env(safe-area-inset-bottom, 0px)));
|
||||||
}
|
}
|
||||||
.fab-secondary {
|
.fab-secondary {
|
||||||
/* sits above the primary: 56 (primary height) + 0.75rem gap + base offset */
|
/* sits above the primary: 56 (primary height) + 0.75rem gap + base offset */
|
||||||
bottom: calc(1.5rem + 56px + 0.75rem + env(safe-area-inset-bottom, 0px));
|
bottom: calc(1.5rem + 56px + 0.75rem + max(var(--kb-inset, 0px), env(safe-area-inset-bottom, 0px)));
|
||||||
}
|
}
|
||||||
.fab.busy {
|
.fab.busy {
|
||||||
animation: fab-pulse 1.4s ease-in-out infinite;
|
animation: fab-pulse 1.4s ease-in-out infinite;
|
||||||
@@ -1140,6 +1143,26 @@
|
|||||||
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the FABs floating above the on-screen keyboard when it's open.
|
||||||
|
// On Chrome+Android the `interactive-widget=resizes-content` meta directive
|
||||||
|
// already shrinks the layout viewport — kb-inset stays 0 there.
|
||||||
|
// On iOS Safari (which ignores the meta) the layout viewport stays full
|
||||||
|
// and we compute the hidden bottom strip from VisualViewport.
|
||||||
|
function updateKbInset() {
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
if (!vv) {
|
||||||
|
document.documentElement.style.setProperty("--kb-inset", "0px");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
||||||
|
document.documentElement.style.setProperty("--kb-inset", inset + "px");
|
||||||
|
}
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.addEventListener("resize", updateKbInset);
|
||||||
|
window.visualViewport.addEventListener("scroll", updateKbInset);
|
||||||
|
}
|
||||||
|
updateKbInset();
|
||||||
|
|
||||||
fillLangs();
|
fillLangs();
|
||||||
loadModels().then(() => refreshLoaded(true));
|
loadModels().then(() => refreshLoaded(true));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user