feat(ux): translate becomes a primary FAB stacked under camera

Promotes Translate from an inline button under the input pane to
the primary 56x56 FAB at the bottom-right corner. Camera FAB
becomes the secondary in the stack, just above. Both share a
single .fab base class; positioning via .fab-primary /
.fab-secondary.

- Translate icon: Material Symbols `translate` (28 px).
- Pulsing-ring `busy` animation while a translation runs, since
  the icon-only button can't show "Translating…" text anymore.
- body padding-bottom bumped to ~9 rem so the output pane
  scrolls clear of the stacked FABs.
- ⌘/Ctrl-Enter still triggers translate via the existing
  CodeMirror keymap; tooltip text updated.
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 19:49:05 +02:00
parent ffa133261d
commit f160c0510b
2 changed files with 30 additions and 12 deletions

View File

@@ -34,7 +34,8 @@
background: var(--bg);
color: var(--fg);
margin: 0;
padding: 2rem 1rem;
/* extra bottom padding so content scrolls clear of the stacked FABs */
padding: 2rem 1rem calc(9rem + env(safe-area-inset-bottom, 0px)) 1rem;
}
.wrap { max-width: 960px; margin: 0 auto; }
h1 {
@@ -165,10 +166,10 @@
user-select: none;
}
/* Floating action button for OCR scan — bottom-right, mobile-thumb friendly */
.scan-fab {
/* Floating action buttons — stacked at bottom-right.
Translate is the primary action (closest to the thumb), Scan sits above. */
.fab {
position: fixed;
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
right: calc(1.5rem + env(safe-area-inset-right, 0px));
width: 56px;
height: 56px;
@@ -184,11 +185,25 @@
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s;
transition: transform 0.15s, box-shadow 0.15s;
}
.fab .material-symbols-outlined { font-size: 28px; }
.fab:hover:not(:disabled) { transform: scale(1.06); }
.fab:disabled { opacity: 0.55; cursor: progress; }
.fab-primary {
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
}
.fab-secondary {
/* 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));
}
.fab.busy {
animation: fab-pulse 1.4s ease-in-out infinite;
}
@keyframes fab-pulse {
0%, 100% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); }
50% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), 0 0 0 8px rgba(224, 122, 62, 0.35); }
}
.scan-fab .material-symbols-outlined { font-size: 28px; }
.scan-fab:hover:not(:disabled) { transform: scale(1.06); }
.scan-fab:disabled { opacity: 0.6; cursor: progress; }
.scan-status {
font-size: 0.75rem;
color: var(--muted);
@@ -408,13 +423,15 @@
</div>
</div>
<button id="go">Translate</button>
<div id="meta" class="meta"></div>
</div>
<button type="button" id="scan-btn" class="scan-fab" aria-label="Scan image (OCR)" title="Scan image (OCR)">
<button type="button" id="scan-btn" class="fab fab-secondary" aria-label="Scan image (OCR)" title="Scan image (OCR)">
<span class="material-symbols-outlined">photo_camera</span>
</button>
<button type="button" id="go" class="fab fab-primary" aria-label="Translate" title="Translate (⌘/Ctrl + Enter)">
<span class="material-symbols-outlined">translate</span>
</button>
<script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
@@ -935,7 +952,7 @@
const text = inputText().trim();
if (!text) return;
goBtn.disabled = true;
goBtn.textContent = "Translating…";
goBtn.classList.add("busy");
output.textContent = "";
output.classList.add("streaming");
@@ -1007,7 +1024,7 @@
} finally {
output.classList.remove("streaming");
goBtn.disabled = false;
goBtn.textContent = "Translate";
goBtn.classList.remove("busy");
}
}